مطالب
Blazor 5x - قسمت 16 - کار با فرم‌ها - بخش 4 - تهیه سرویس‌های آپلود تصاویر
در ادامه می‌خواهیم برای هر اتاق ثبت شده، تعدادی تصویر مرتبط را نیز به سرور آپلود کرده و مشخصات آن‌ها را در بانک اطلاعاتی ثبت کنیم. به همین جهت در این قسمت سرویس ثبت اطلاعات تصاویر در بانک اطلاعاتی و سرویس آپلود فایل‌ها را تهیه می‌کنیم.


تعریف موجودیت و DbSet تصاویر یک اتاق هتل

برای اینکه بتوان اطلاعات تصاویر آپلودی را در بانک اطلاعاتی ثبت کرد، نیاز است یک رابطه‌ی یک به چند را بین یک اتاق و تصاویر مرتبط با آن برقرار کرد. به همین جهت ابتدا به پروژه‌ی BlazorServer.Entities.csproj مراجعه کرده و موجودیت ثبت اطلاعات تصاویر را تعریف می‌کنیم:
using System.ComponentModel.DataAnnotations.Schema;

namespace BlazorServer.Entities
{
    public class HotelRoomImage
    {
        public int Id { get; set; }

        public string RoomImageUrl { get; set; }

        [ForeignKey("RoomId")]
        public virtual HotelRoom HotelRoom { get; set; }
        public int RoomId { get; set; }
    }
}
که در اینجا باید سر دیگر این رابطه‌ی one-to-many، در جدول HotelRoom نیز تعریف شود:
namespace BlazorServer.Entities
{
    public class HotelRoom
    {
        // ...
        public virtual ICollection<HotelRoomImage> HotelRoomImages { get; set; }
    }
}
در آخر باید این موجودیت جدید را به Context برنامه معرفی کرد. برای اینکار به پروژه‌ی BlazorServer.DataAccess مراجعه کرده و DbSet متناظری را تعریف می‌کنیم:
namespace BlazorServer.DataAccess
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<HotelRoomImage> HotelRoomImages { get; set; }

        // ...
    }
}
پس از این تغییرات، نیاز است یکبار دیگر عملیات Migrations را اجرا کرد، تا ساختار متناظر بانک اطلاعاتی این تغییرات ایجاد شود. بنابراین توسط خط فرمان به پوشه‌ی پروژه‌ی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا می‌کنیم. در اینجا نگارش 5.0.3 باید معادل نگارشی از EF-Core باشد که از آن در حال استفاده‌اید:
dotnet tool update --global dotnet-ef --version 5.0.3
dotnet build
dotnet ef migrations --startup-project ../BlazorServer.App/ add Init --context ApplicationDbContext
dotnet ef --startup-project ../BlazorServer.App/ database update --context ApplicationDbContext
در مورد این دستورات در قسمت 13 بیشتر بحث شده‌است.


تعریف مدل UI متناظر با هر تصویر

همانطور که در قسمت 13 نیز عنوان شد، در حین کار با رابط کاربری برنامه، با موجودیت‌های بانک اطلاعاتی، به صورت مستقیم کار نخواهیم کرد و بر اساس نیازهای برنامه، یکسری کلاس DTO را تعریف می‌کنیم. بنابراین به پروژه‌ی BlazorServer.Models مراجعه کرده و DTO متناظر با HotelRoomImage را به صورت زیر اضافه می‌کنیم:
namespace BlazorServer.Models
{
    public class HotelRoomImageDTO
    {
        public int Id { get; set; }

        public int RoomId { get; set; }

        public string RoomImageUrl { get; set; }
    }
}
و همچنین جهت سهولت تبدیل اطلاعات بین موجودیت تعریف شده و DTO ی آن، نگاشت AutoMapper دو طرفه‌ای را در پروژه‌ی BlazorServer.Models.Mappings برقرار می‌کنیم:
using AutoMapper;
using BlazorServer.Entities;

namespace BlazorServer.Models.Mappings
{
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            // ...
            CreateMap<HotelRoomImageDTO, HotelRoomImage>().ReverseMap(); // two-way mapping
        }
    }
}

تعریف سرویس کار با HotelRoomImage

در اینجا نیز همانند سرویسی که برای انجام عملیات تجاری مرتبط با یک اتاق هتل، در قسمت 13 پیاده سازی کردیم، سرویس دیگری را در پروژه‌ی BlazorServer.Services برای کار با تصاویر اتاق‌ها تهیه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomImageService
    {
        Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO);

        Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId);

        Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId);

        Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId);
    }
}
برای نمونه بر اساس اطلاعات مدل UI برنامه، نیاز است بتوانیم اطلاعات یک تصویر را ثبت و یا حذف کنیم و یا لیست تصاویر یک اتاق را از بانک اطلاعاتی دریافت کنیم؛ با این پیاده سازی:
namespace BlazorServer.Services
{
    public class HotelRoomImageService : IHotelRoomImageService
    {
        private readonly ApplicationDbContext _dbContext;
        private readonly IMapper _mapper;
        private readonly IConfigurationProvider _mapperConfiguration;

        public HotelRoomImageService(ApplicationDbContext dbContext, IMapper mapper)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
            _mapperConfiguration = mapper.ConfigurationProvider;
        }

        public async Task<int> CreateHotelRoomImageAsync(HotelRoomImageDTO imageDTO)
        {
            var image = _mapper.Map<HotelRoomImage>(imageDTO);
            await _dbContext.HotelRoomImages.AddAsync(image);
            return await _dbContext.SaveChangesAsync();
        }

        public async Task<int> DeleteHotelRoomImageByImageIdAsync(int imageId)
        {
            var image = await _dbContext.HotelRoomImages.FindAsync(imageId);
            _dbContext.HotelRoomImages.Remove(image);
            return await _dbContext.SaveChangesAsync();
        }

        public async Task<int> DeleteHotelRoomImageByRoomIdAsync(int roomId)
        {
            var imageList = await _dbContext.HotelRoomImages.Where(x => x.RoomId == roomId).ToListAsync();
            _dbContext.HotelRoomImages.RemoveRange(imageList);
            return await _dbContext.SaveChangesAsync();
        }

        public Task<List<HotelRoomImageDTO>> GetHotelRoomImagesAsync(int roomId)
        {
            return _dbContext.HotelRoomImages
                            .Where(x => x.RoomId == roomId)
                            .ProjectTo<HotelRoomImageDTO>(_mapperConfiguration)
                            .ToListAsync();
        }
    }
}
پس از این تعاریف، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و این سرویس را به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IHotelRoomImageService, HotelRoomImageService>();
            // ...


تهیه سرویسی برای آپلود فایل‌های یک برنامه‌ی Blazor Server به سرور

جهت ساده سازی کار آپلود، در برنامه‌های Blazor Server، سرویس جدید FileUploadService را به پروژه‌ی BlazorServer.Services اضافه می‌کنیم:
using Microsoft.AspNetCore.Components.Forms;
using System.Threading.Tasks;

namespace BlazorServer.Services
{
    public interface IFileUploadService
    {
        void DeleteFile(string fileName, string webRootPath, string uploadFolder);
        Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder);
    }
}
کار آن حذف یک فایل، بر اساس مسیر آن است و همچنین دریافت یک IBrowserFile از کاربر و ذخیره سازی اطلاعات آن در سرور؛ با این پیاده سازی:
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.IO;
using System.Threading.Tasks;

namespace BlazorServer.Services
{
    public class FileUploadService : IFileUploadService
    {
        private const int MaxBufferSize = 0x10000;

        public void DeleteFile(string fileName, string webRootPath, string uploadFolder)
        {
            var path = Path.Combine(webRootPath, uploadFolder, fileName);
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }

        public async Task<string> UploadFileAsync(IBrowserFile inputFile, string webRootPath, string uploadFolder)
        {
            createUploadDir(webRootPath, uploadFolder);
            var (fileName, imageFilePath) = getOutputFileInfo(inputFile, webRootPath, uploadFolder);

            using (var outputFileStream = new FileStream(
                        imageFilePath, FileMode.Create, FileAccess.Write,
                        FileShare.None, MaxBufferSize, useAsync: true))
            {
                using var inputStream = inputFile.OpenReadStream();
                await inputStream.CopyToAsync(outputFileStream);
            }

            return $"{uploadFolder}/{fileName}";
        }

        private static (string FileName, string FilePath) getOutputFileInfo(
                    IBrowserFile inputFile, string webRootPath, string uploadFolder)
        {
            var fileName = Path.GetFileName(inputFile.Name);
            var imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName);
            if (File.Exists(imageFilePath))
            {
                var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
                var fileExtension = Path.GetExtension(fileName);
                fileName = $"{fileNameWithoutExtension}-{Guid.NewGuid()}{fileExtension}";
                imageFilePath = Path.Combine(webRootPath, uploadFolder, fileName);
            }
            return (fileName, imageFilePath);
        }

        private static void createUploadDir(string webRootPath, string uploadFolder)
        {
            var folderDirectory = Path.Combine(webRootPath, uploadFolder);
            if (!Directory.Exists(folderDirectory))
            {
                Directory.CreateDirectory(folderDirectory);
            }
        }
    }
}
اگر در ASP.NET Core، اطلاعات فایل ارسالی به سرور، توسط IFormFile به اکشن متدهای کنترلرها ارسال می‌شود، در برنامه‌های Blazor Server اینکار توسط IBrowserFile صورت می‌گیرد. کلیات کار با آن، بسیار شبیه به IFormFile است و اگر به مطلب «بررسی روش آپلود فایل‌ها در ASP.NET Core» مراجعه کنید، تفاوت آنچنانی را مشاهده نخواهید کرد. تنها تفاوت پیاده سازی که در اینجا وجود دارد، نیاز به استفاده‌ی از متد ()inputFile.OpenReadStream جهت دسترسی به محتوای فایل آپلودی، برای ذخیره‌ی آن در سمت سرور است؛ وگرنه مابقی کدهای آپلود آن، با ASP.NET Core یکی است.
همچنین برای دسترسی به IBrowserFile در یک سرویس، نیاز است وابستگی زیر را نیز به پروژه‌ی سرویس‌ها اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.3" />
  </ItemGroup>
</Project>
پس از آن، به فایل BlazorServer\BlazorServer.App\Startup.cs مراجعه کرده و این سرویس را به سیستم تزریق وابستگی‌های برنامه معرفی می‌کنیم:
namespace BlazorServer.App
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IFileUploadService, FileUploadService>();
            // ...
در قسمت بعد، از این سرویس‌ها جهت مدیریت آپلود تصاویر استفاده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-16.zip
مطالب
اندازه گیری کارآیی پرس و جوها با استفاده از SET STATISTICS TIME
یکی از وظایف اصلی مدیر و یا توسعه دهنده یک بانک اطلاعاتی، نوشتن کدهای T-SQL و اندازه‌گیری عملکرد آنها می‌باشد. ابزارهای مختلفی برای انجام این کار وجود دارد، چه آنهایی که در خود SQL Server بصورت محلی وجود دارند و چه آنهایی که توسط شرکت‌های ثالث ارائه می‌شوند. اما مسئله مهمی که باید در نظر بگیرید چگونگی نوشتن یک پرس و جو (Query) و اندازه گیری کارآیی آن می‌باشد و اینکه باید روی چه مواردی متمرکز شد. در اکثر مواقع گرفتن زمان اجرای یک پرس و جو تا اندازه‌ای خوب می‌باشد. یکی از مواردی که باید روی آن متمرکز شد منابع استفاده شده توسط سرور می‌باشد، درحالیکه زمان اجرای پرس وجو به پارامترهای دیگری همچون بار سرور نیز بستگی دارد. علاوه بر استفاده از پروفایلر و نقشه اجرای کوئری (Execution Plan) ، می‌توانید از SET STATISTICS TIME نیز استفاده نمایید.
 SET STATISTICS TIME تنظیمی است که برای اندازه گیری منابع مورد نیاز اجرای یک پرس و جو به شما کمک می‌کند. SET STATISTICS TIME آمار مربوط به زمان تجزیه و تحلیل (Parse)، کامپایل و اجرای هر دستور در یک پرس و جو را نمایش می‌دهد. راه‌های مختلف اندکی برای مقایسه آماری دو پرس و جو و انتخاب بهترین آنها برای استفاده وجود دارند.  
برای روشن کردن این تنظیم دو راه وجود دارد. ابتدا اینکه از دستور Set برای روشن و خاموش کردن استفاده نمایید و یا اینکه از طریق Query Analyzer اقدام به انجام این کار نمایید. 
SET STATISTICS TIME ON
برای اینکه بتوانید آمارهای این تنظیمات را مشاهده کنید می‌بایست قبل از اجرای پرس و جو تنظیم مورد نظر را روشن نمایید. در نظر داشته باشید که با روشن کردن این تنظیم، برای تمامی پرس و جوهای مربوط به آن جلسه (Session) روشن خواهد ماند تا زمانیکه آنرا خاموش نمایید. 
SELECT ProductID, StartDate, EndDate, StandardCost 
FROM Production.ProductCostHistory
WHERE StandardCost < 500.00; 
با اجرای دستورات بالا خروجی آن بصورت زیر می‌باشد:
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 1 ms.

(269 row(s) affected)

SQL Server Execution Times:
   CPU time = 1 ms,  elapsed time = 2 ms. 
زمان صرف شده برای اجرای یک کوئری به دو بخش تقسیم می‌شود:
  • زمان کامپایل و تجزیه و تحلیل ( parse and compile time)  زمانیکه یک کوئری را برای اجرا به SQL Server  ارائه می‌دهید، SQL Server آنرا از نظر خطای نحوی بررسی می‌نماید و بهینه ساز یک نقشه بهینه را برای اجرا تولید می‌نماید. اگر به خروجی نگاه کنید، زمان پردازش ( CPU time) و زمان سپری شده ( elapsed time)  را نشان می دهد. منظور از زمان پردازش زمان واقعی صرف شده روی پردازنده می‌باشد و زمان سپری شده اشاره به زمان تکمیل شدن عملیات کامپایل و تجزیه و تحلیل می‌باشد. تفاوت بین زمان پردازش و زمان سپری شده ممکن است زمان انتظار در صف برای گرفتن پردازنده و یا انتظار برای تکمیل عملیات IO باشد. 

  • زمان اجرا  ( Execution Times) : این زمان اشاره به زمان سپری شده برای تکمیل اجرای نقشه کامپایل شده دارد. زمان پردازش اشاره به زمان واقعی صرف شده روی پردازنده دارد و زمان سپری شده نیز مجموع زمان صرف شده تا تکمیل اجرای دستور که شامل زمان انتظار برای تکمیل عملیات IO و زمان صرف شده برای انتقال خروجی به کلاینت می‌باشد، دارد. زمان پردازش می‌تواند به عنوان مبنایی برای اندازه‌گیری کارآیی مورد استفاده قرار بگیرد. این مقدار در اجراهای مختلف تفاوت چندانی با هم ندارند جز اینکه کوئری و یا داده‌ها تغییر نمایند. توجه نمایید که زمان براساس میلی ثانیه می‌باشد. زمان سپری شده نیز به فاکتورهایی مانند بارگذاری روی سرور، بارگذاری IO، و پهنای باند بین سرور و کلاینت وابسته می‌باشد. بنابراین همیشه زمان پردازش به عنوان مبنایی برای اندازه‌گیری کارایی استفاده می‌شود .

در این بخش به بررسی SET STATISTICS TIME  در SQL Server پرداختیم. در بخش بعدی به بررسی   SET STATISTICS IO  برای اندازه گیری کارایی پرس و جوها از نظر میزان استفاده IO خواهیم پرداخت.
نظرات مطالب
مشکل همزمانی خواندن و به روز رسانی اطلاعات در برنامه‌های وب

خیر و بهتر است اگر نیاز به انجام چندین عملیات را در این بین دارید، «... به‌صورت صریحی، یک تراکنش مشخص، برای کل این مجموعه باز شود ...». قسمت «د) پیاده سازی optimistic locking» انتهای این بحث، فقط برای حالتی است که عملیات انجام شده، تحت نظر سیستم Tracking یک ORM قرار می‌گیرد و «Bulk operations» جزو آن نیست.

مطالب
React 16x - قسمت 21 - کار با فرم‌ها - بخش 4 - چند تمرین
پس از فراگیری اصول کار کردن با فرم‌ها در React، اکنون می‌خواهیم چند فرم جدید را برای تمرین بیشتر، به برنامه‌ی نمایش لیست فیلم‌ها اضافه کنیم؛ مانند فرم ثبت نام، فرمی برای ثبت و یا ویرایش فیلم‌ها و یک فرم جستجوی سریع در لیست فیلم‌های موجود.

تمرین 1 - ایجاد فرم ثبت نام


می‌خواهیم به برنامه، فرم ثبت نام را که حاوی سه فیلد نام کاربری، کلمه‌ی عبور و نام است، اضافه کنیم. نام کاربری باید از نوع ایمیل باشد. بنابراین اعتبارسنجی مرتبطی نیز باید برای این فیلد تعریف شود. کلمه‌ی عبور وارد شده باید حداقل 5 حرف باشد. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشده‌است، باید دکمه‌ی submit فرم، غیرفعال باقی بماند. لینک ورود به این فرم نیز باید به منوی راهبری سایت اضافه شود.

برای حل این تمرین، فایل جدید registerForm.jsx را در پوشه‌ی components ایجاد می‌کنیم و سپس توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت RegisterForm را ایجاد کرده و سپس آن‌را به صورت زیر تکمیل می‌کنیم:
- ابتدا در فایل app.js، پس از import ماژول آن:
import RegisterForm from "./components/registerForm";
در ابتدای سوئیچ تعریف شده، مسیریابی آن‌را تعریف می‌کنیم:
<Route path="/register" component={RegisterForm} />
- سپس در فایل src\components\navBar.jsx، لینک به آن‌را، در انتهای لیست اضافه می‌کنیم، تا در منوی راهبری ظاهر شود:
<NavLink className="nav-item nav-link" to="/register">
   Register
</NavLink>
- در ادامه کدهای کامل کامپوننت ثبت نام را ملاحظه می‌کنید:
import Joi from "@hapi/joi";
import React from "react";

import Form from "./common/form";

class RegisterForm extends Form {
  state = {
    data: { username: "", password: "", name: "" },
    errors: {}
  };

  schema = {
    username: Joi.string()
      .required()
      .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } })
      .label("Username"),
    password: Joi.string()
      .required()
      .min(5)
      .label("Password"),
    name: Joi.string()
      .required()
      .label("Name")
  };

  doSubmit = () => {
    // Call the server
    console.log("Submitted");
  };

  render() {
    return (
      <div>
        <h1>Register</h1>
        <form onSubmit={this.handleSubmit}>
          {this.renderInput("username", "Username")}
          {this.renderInput("password", "Password", "password")}
          {this.renderInput("name", "Name")}
          {this.renderButton("Register")}
        </form>
      </div>
    );
  }
}

export default RegisterForm;
- ابتدا این کامپوننت را بجای ارث بری از Component خود React، از کامپوننت Form که در قسمت قبل ایجاد کردیم، ارث بری می‌کنیم تا به تمام امکانات آن مانند اعتبارسنجی، مدیریت حالت و متدهای کمکی تعریف فیلدها و دکمه‌ها بهره‌مند شویم.
- سپس state این کامپوننت را با شیءای حاوی دو خاصیت data و error، مقدار دهی اولیه می‌کنیم. خواص متناظر با المان‌های فرم را نیز به صورت یک شیء، به خاصیت data انتساب داده‌ایم.
- پس از آن، خاصیت schema تعریف شده‌است؛ تا قواعد اعتبارسنجی تک تک فیلدهای فرم را به کمک کتابخانه‌ی Joi، مطابق نیازمندی‌هایی که در ابتدای تعریف این تمرین مشخص کردیم، ایجاد کند.
- در ادامه، متد doSubmit را ملاحظه می‌کنید. این متد پس از کلیک بر روی دکمه‌ی Register و پس از اعتبارسنجی موفقیت آمیز فرم، به صورت خودکار فراخوانی می‌شود.
- در آخر، تعریف فرم ثبت‌نام را مشاهده می‌کنید که نکات آن‌را در قسمت قبل، با معرفی کامپوننت Form و افزودن متدهای کمکی رندر input و button به آن، بررسی کردیم و در کل با نکات بررسی شده‌ی در فرم لاگینی که تا به اینجا ایجاد کردیم، تفاوتی ندارد.


تمرین 2- ایجاد فرم ثبت و یا ویرایش یک فیلم


فرم جدید ثبت و ویرایش یک فیلم، نکات بیشتری را به همراه دارد. در اینجا می‌خواهیم در بالای لیست نمایش فیلم‌ها، یک دکمه‌ی new movie را اضافه کنیم تا با کلیک بر روی آن، به فرم ثبت و ویرایش فیلم‌ها هدایت شویم. این فرم، از فیلدهای یک عنوان متنی، انتخاب ژانر از یک drop down list، تعداد موجود (بین 1 و 100) و امتیاز (بین صفر تا 10) تشکیل شده‌است. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشده‌است، دکمه‌ی submit فرم باید غیرفعال باقی بماند. پس از ذخیره شدن این فیلم (در لیست درون حافظه‌ای برنامه)، با مراجعه‌ی به لیست فیلم‌ها و انتخاب آن از لیست (با کلیک بر روی لینک آن)، باید مجددا به همین فرم، در حالت ویرایش این رکورد هدایت شویم. به علاوه اگر در بالای صفحه یک id اشتباه وارد شد، باید صفحه‌ی «پیدا نشد» نمایش داده شود.

کامپوننت MovieForm و مسیریابی آن‌را در قسمت 17، تعریف و اضافه کردیم. برای تعریف لینکی به آن، به کامپوننت movies مراجعه کرده و بالای متنی که تعداد کل آیتم‌های موجود در بانک اطلاعاتی را نمایش می‌دهد، المان زیر را اضافه می‌کنیم:
import { Link } from "react-router-dom";
// ...


<div className="col">
  <Link
    to="/movies/new"
    className="btn btn-primary"
    style={{ marginBottom: 20 }}
  >
    New Movie
  </Link>
  <p>Showing {totalCount} movies in the database.</p>
این Link را هم با کلاس btn مزین کرده‌ایم تا شبیه به یک دکمه، به نظر برسد. با کلیک بر روی آن، به آدرس movies/new هدایت خواهیم شد؛ یعنی id جدید این مسیریابی را به "new" تنظیم کرده‌ایم که در ادامه بر اساس آن، تفاوت بین حالت ویرایش و حالت ثبت اطلاعات، مشخص می‌شود.


سپس به کامپوننت src\components\movieForm.jsx که پیشتر آن‌را اضافه کرده بودیم، مراجعه کرده و به صورت زیر آن‌را تکمیل می‌کنیم:
import Joi from "@hapi/joi";
import React from "react";

import { getGenres } from "../services/fakeGenreService";
import { getMovie, saveMovie } from "../services/fakeMovieService";
import Form from "./common/form";

class MovieForm extends Form {
  state = {
    data: {
      title: "",
      genreId: "",
      numberInStock: "",
      dailyRentalRate: ""
    },
    genres: [],
    errors: {}
  };
- ابتدا importهای مورد نیاز به Joi، React و همچنین سرویس‌های لیست فیلم‌ها و لیست ژانرهای سینمایی، به همراه کامپوننت فرم، تعریف شده‌اند.
- سپس این کامپوننت نیز از کامپوننت Form ارث بری می‌کند تا به امکانات ویژه‌ی آن دسترسی پیدا کند.
- در ادامه در خاصیت state، طبق روالی که در کامپوننت فرم درنظر گرفته‌ایم، دو خاصیت data و errors باید حضور داشته باشند. در خاصیت data، شیءای که نام خاصیت‌های آن با فیلدهای فرم تطابق دارد، ذکر شده‌اند. در اینجا برای ذخیره سازی اطلاعات انتخاب شده‌ی از drop down list مرتبط با ژانرهای سینمایی، از خاصیت genreId استفاده می‌شود؛ این تنها اطلاعاتی است که از کل آیتم‌های یک drop down list نیاز داریم. آرایه‌ی genres که آیتم‌های این drop down list را مقدار دهی می‌کند، در روال componentDidMount، از سرویس مرتبطی دریافت و مقدار دهی خواهد شد.

در ادامه‌ی کدهای کامپوننت MovieForm، کدهای schema اعتبارسنجی شیء data را ملاحظه می‌کنید:
  schema = {
    _id: Joi.string(),
    title: Joi.string()
      .required()
      .label("Title"),
    genreId: Joi.string()
      .required()
      .label("Genre"),
    numberInStock: Joi.number()
      .required()
      .min(0)
      .max(100)
      .label("Number in Stock"),
    dailyRentalRate: Joi.number()
      .required()
      .min(0)
      .max(10)
      .label("Daily Rental Rate")
  };
در اینجا، id به required تنظیم نشده‌است؛ چون زمانیکه قرار است یک شیء movie جدید را  ایجاد کنیم، هنوز این id نامشخص است. سایر موارد خاصیت schema، به لطف fluent api کتابخانه‌ی Joi، بسیار خوانا بوده و نیاز به توضیحات خاصی ندارند. برای مثال هر دو خاصیت numberInStock و  dailyRentalRate باید عددی وارد شده و بین بازه‌ی مشخصی قرار گیرند.

اکنون به مرحله‌ی componentDidMount می‌رسیم:
  componentDidMount() {
    const genres = getGenres();
    this.setState({ genres });

    const movieId = this.props.match.params.id;
    if (movieId === "new") return;

    const movie = getMovie(movieId);
    if (!movie) return this.props.history.replace("/not-found");

    this.setState({ data: this.mapToViewModel(movie) });
  }
- در اینجا لیست ژانرهای سینمایی از متد getGenres فایل src\services\fakeGenreService.js دریافت شده و پس از آن کار به روز رسانی خاصیت genres در state را انجام می‌دهیم. این به روز رسانی state، سبب می‌شود تا این خاصیت که آرایه‌ای است، در رندر بعدی این کامپوننت، به لیست options مربوط به drop down list درج شده‌ی در فرم، ارسال شده و در فرم رندر شود.
- پس از آن، نحوه‌ی دریافت پارامتر id مسیریابی رسیده را ملاحظه می‌کنید. این id اگر به "new" تنظیم شده بود، یعنی قرار است، اطلاعات جدیدی ثبت شوند. بنابراین متد جاری را خاتمه می‌دهیم (چون کار ادامه‌ی این متد، مقدار دهی اولیه‌ی تمام فیلدهای فرم، بر اساس اطلاعات شیء دریافت شد‌ه‌ی از سرویس فیلم‌ها است). در غیراینصورت (و با مشخص بودن id)، با استفاده از این id و متد getMovie سرویس src\services\fakeMovieService.js، سعی خواهیم کرد تا اطلاعات شیء movie متناظری را دریافت کنیم. اگر خروجی این متد null بود، یعنی id وارد شده معتبر نیست. به همین جهت کاربر را به صفحه‌ی not-found هدایت می‌کنیم. اگر دقت کنید در اینجا بجای متد push، از متد replace استفاده کرده‌ایم. چون اگر از متد push استفاده می‌کردیم و کاربر بر روی دکمه‌ی back مرورگر کلیک می‌کرد، دوباره به همین صفحه، با id غیرمعتبر قبلی وارد می‌شد و یک حلقه‌ی بی‌پایان رخ می‌داد. همچنین به return ای هم که به همراه متد replace استفاده شده، دقت کنید. کار redirect به یک صفحه‌ی دیگر، به معنای عدم اجرای کدهای پس از آن نیست. بنابراین اگر می‌خواهیم کار این متد با redirect، به پایان برسد، ذکر return الزامی است.
- در پایان این متد، خاصیت data موجود در state را به روز رسانی می‌کنیم؛ تا سبب رندر فرم، با اطلاعات شیء movie یافت شده گردد و چون ساختار شیء movie دریافت شده‌ی از سرویس، با ساختار data تعریف شده‌ی در state یکی نیست، نیاز به نگاشت این دو به هم، توسط متد سفارشی mapToViewModel زیر است:
  mapToViewModel(movie) {
    return {
      _id: movie._id,
      title: movie.title,
      genreId: movie.genre._id,
      numberInStock: movie.numberInStock,
      dailyRentalRate: movie.dailyRentalRate
    };
  }
این سناریو بسیار متداول است و اکثر داده‌های دریافت شده‌ی از سرور، الزاما با ساختار داده‌هایی که در فرم‌های خود تعریف می‌کنیم (که در اینجا view-model نام گرفته)، یکی نیستند و نیاز به نگاشت بین آن‌ها وجود دارد. برای مثال genreId موجود در view-model این فرم (همان شیء منتسب به data در state)، دقیقا به همین نام، در شیء movie تعریف نشده‌است و نیاز به نگاشت این دو به هم است.

در ادامه‌ی کدهای کامپوننت فرم فیلم‌ها، به متد doSubmit می‌رسیم:
  doSubmit = () => {
    saveMovie(this.state.data);

    this.props.history.push("/movies");
  };
این متد پس از کلیک کاربر بر روی دکمه‌ی submit و اعتبارسنجی کامل فرم، فراخوانی می‌شود. در این مرحله می‌توان اطلاعات موجود در شیء data را به متد saveMovie سرویس src\services\fakeMovieService.js ارسال کرد، تا آن‌را به لیست خودش اضافه کند. سپس کاربر را به لیست به روز شده‌ی فیلم‌ها هدایت می‌کنیم.

در انتهای این کامپوننت نیز به متد رندر آن می‌رسیم:
  render() {
    return (
      <div>
        <h1>Movie Form</h1>
        <form onSubmit={this.handleSubmit}>
          {this.renderInput("title", "Title")}
          {this.renderSelect("genreId", "Genre", this.state.genres)}
          {this.renderInput("numberInStock", "Number in Stock", "number")}
          {this.renderInput("dailyRentalRate", "Rate")}
          {this.renderButton("Save")}
        </form>
      </div>
    );
  }
تمام قسمت‌های این فرم را منهای متد جدید renderSelect آن، پیشتر در قسمت قبل، مرور کرده‌ایم و نکته‌ی جدیدی ندارند.
برای تعریف متد جدید renderSelect به این صورت عمل می‌کنیم:
- ابتدا فایل جدید src\components\common\select.jsx را ایجاد کرده و سپس آن‌را جهت نمایش یک drop down list، ویرایش می‌کنیم:
import React from "react";

const Select = ({ name, label, options, error, ...rest }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <select name={name} id={name} {...rest} className="form-control">
        <option value="" />
        {options.map(option => (
          <option key={option._id} value={option._id}>
            {option.name}
          </option>
        ))}
      </select>
      {error && <div className="alert alert-danger">{error}</div>}
    </div>
  );
};

export default Select;
شبیه به یک چنین کامپوننتی را در قسمت قبل، در فایل src\components\common\input.jsx ایجاد کردیم و ساختار کلی آن‌ها با هم یکی است. ابتدا تمام تگ‌ها و کلاس‌های بوت استرپی مورد نیاز، در این کامپوننت محصور می‌شوند. سپس آرایه‌ای بر روی لیست options رسیده، ایجاد شده و به صورت پویا، لیست نمایش داده شده‌ی توسط drop down آن‌را تشکیل می‌دهد. در پایان آن هم کار نمایش اخطار اعتبارسنجی متناظری، در صورت وجود خطایی، قرار گرفته‌است.

- پس از آن به کامپوننت src\components\common\form.jsx مراجعه کرده و متد رندر آن‌را اضافه می‌کنیم:
import Select from "./select";
// ...

class Form extends Component {

  // ...

  renderSelect(name, label, options) {
    const { data, errors } = this.state;

    return (
      <Select
        name={name}
        value={data[name]}
        label={label}
        options={options}
        onChange={this.handleChange}
        error={errors[name]}
      />
    );
  }
}
کار این متد، مقدار دهی ویژگی‌های مورد نیاز کامپوننت Select، بر اساس نام فیلد، یک برچسب و آیتم‌های ارسالی به آن است. مزیت وجود یک چنین متد کمکی، کم شدن کدهای تکراری Selectهای مورد نیاز و همچنین عدم فراموشی قسمتی از این اتصالات و در نهایت یک‌دست شدن کدهای کل برنامه‌است. این متد در نهایت سبب رندر یک drop down list، بر اساس اطلاعات خاصیت genres موجود در state می‌شود:



تمرین 3- جستجوی در لیست فیلم‌ها


می‌خواهیم در بالای لیست نمایش فیلم‌ها، یک search box را قرار دهیم تا توسط آن بتوان بر اساس عنوان وارد شده، در فیلم‌های موجود جستجو کرد. همچنین این جستجو قرار است کلی بوده و حتی در صورت انتخاب ژانر خاصی از منوی کنار صفحه، باید در کل اطلاعات موجود جستجو کند. به علاوه اگر کاربر ژانری را انتخاب کرد، این text box باید خالی شود.

برای اینکار ابتدا فایل جدید src\components\searchBox.jsx را ایجاد کرده و به صورت زیر آن‌را تکمیل می‌کنیم:
import React from "react";

const SearchBox = ({ value, onChange }) => {
  return (
    <input
      type="text"
      name="query"
      className="form-control my-3"
      placeholder="Search..."
      value={value}
      onChange={e => onChange(e.currentTarget.value)}
    />
  );
};

export default SearchBox;
این SeachBox، یک controlled component است و دارای state خاص خودش نیست. تمام اطلاعات مورد نیاز خود را از طریق props دریافت کرده و خروجی خود را (اطلاعات تایپ شده‌ی در input box را) از طریق صدور رخ‌دادها، اطلاع رسانی می‌کند.

سپس به کامپوننت movies مراجعه کرده و آن‌را ذیل متن نمایش تعداد رکوردها، درج می‌کنیم:
<p>Showing {totalCount} movies in the database.</p>
<SearchBox value={searchQuery} onChange={this.handleSearch} />
که البته نیاز به import کامپوننت مربوطه، تعریف واژه‌ی جستجو شده در state و مدیریت رخ‌داد onChange را نیز دارد:
import SearchBox from "./searchBox";
//...

class Movies extends Component {
  state = {
    //...
    selectedGenre: {},
    searchQuery: ""
  };


  handleSearch = query => {
    this.setState({ searchQuery: query, selectedGenre: null, currentPage: 1 });
  };

  handleGenreSelect = genre => {
    console.log("handleGenreSelect", genre);
    this.setState({ selectedGenre: genre, searchQuery: "", currentPage: 1 });
  };
در متد handleSearch، اطلاعات وارد شده‌ی توسط کاربر دریافت شده و توسط آن سه خاصیت state به روز رسانی می‌شوند تا توسط آن‌ها در حین رندر مجدد کامپوننت، کار فیلتر صحیح اطلاعات صورت گیرد. همچنین selectedGenre نیز به حالت اول بازگشت داده می‌شود. به علاوه اگر کاربر در حین مشاهده‌ی صفحه‌ی 3 بود، نیاز است currentPage صحیحی را به او نمایش  داد.
متد handleGenreSelect را نیز اندکی تغییر داده‌ایم تا اگر گروهی انتخاب شد، مقدار searchQuery را خالی کند. اگر در اینجا searchQuery را به نال تنظیم می‌کردیم، controlled component جعبه‌ی جستجو، تبدیل به کامپوننت کنترل نشده‌ای می‌شد و در این حالت، React، اخطار تبدیل بین این دو را صادر می‌کرد.

در آخر، ابتدای متد getPageData هم جهت اعمال searchQuery، به صورت زیر تغییر می‌کند:
  getPagedData() {
    const {
      pageSize,
      currentPage,
      selectedGenre,
      movies: allMovies,
      sortColumn,
      searchQuery
    } = this.state;

    let filteredMovies = allMovies;
    if (searchQuery) {
      filteredMovies = allMovies.filter(m =>
        m.title.toLowerCase().startsWith(searchQuery.toLowerCase())
      );
    } else if (selectedGenre && selectedGenre._id) {
      filteredMovies = allMovies.filter(m => m.genre._id === selectedGenre._id);
    }
در اینجا اگر searchQuery مقداری داشته باشد، یک جستجوی غیرحساس به کوچکی و بزرگی حروف، بر روی خاصیت title اشیاء فیلم، انجام می‌شود.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-21.zip
نظرات مطالب
طراحی گردش کاری با استفاده از State machines - قسمت سوم
- مورد استفاده در هر شرکتی با بیش از یک نفر کارمند.
مانند گردش کاری درخواست:
  1. مساعده
  2. مرخصی
  3. ماموریت
  4. تائید ساعات کاری
  5. درخواست و تائید تشکیل یک جلسه
  6. پر کردن و تائید تایم شیت
و ... تمام کارهای یک سازمان یا شرکت
-  هدف آشنایی بصری شما با نحوه حل مسایل چند مرحله‌ای که در ابتدا ساده به نظر می‌رسند، اما 10 مرحله که به آن اضافه شود، مدیریت آن به روش‌های متداول طاقت فرسا خواهد شد.
- وجود این تصاویر، تولید کننده کد و امثال آن صرفا برای ساده کردن توضیح انبوهی متن، به همراه روشی برای حل آن بود.
مطالب
بررسی تغییرات Blazor 8x - قسمت هشتم - مدیریت انتقال اطلاعات Pre-Rendering سمت سرور، به جزایر تعاملی
این Prerendering است که امکان رندر یک کامپوننت تعاملی را در سمت سرور میسر می‌کند تا کاربر بتواند پیش از فعال شدن قابلیت‌های پیشرفته‌ی یک کامپوننت، یک حداقل خروجی را از آن مشاهده کند و همچنین وجود آن برای موتورهای جستجو و بهبود SEO بسیار مفید است. اما ... در این بین مشکلی رخ می‌دهد که نمونه‌ی آن‌را در قسمت قبل مشاهده کردیم: آغاز آن دوبار صورت می‌گیرد؛ یکبار در سمت سرور برای تهیه‌ی یک خروجی SSR و یکبار هم پس از فعال شدن قابلیت‌های تعاملی آن در سمت کلاینت. این آغاز دوباره، برای هر دو حالت کامپوننت‌های تعاملی Blazor Server و Blazor WASM برقرار است. راه‌حل‌هایی از نحوه‌ی مواجه شدن با یک چنین مشکلی را در قسمت قبل بررسی کردیم. راه‌حل دیگری که در این بین ارائه شده و توسط خود مایکروسافت هم در مثال‌های آن مورد استفاده قرار می‌گیرد، استفاده از سرویس PersistentComponentState است که جزئیات آن‌را در این قسمت بررسی خواهیم کرد.


بررسی نحوه‌ی عملکرد سرویس PersistentComponentState

سرویس PersistentComponentState، در دات‌نت 6، به Blazor اضافه شد و امکان جدیدی نیست. قسمتی از این مباحث جدید SSR که به‌نظر مختص به Blazor 8x هستند، پیشتر هم وجود داشتند؛ تحت عنوان pre-rendering. برای مثال فقط کافی بودن تا در برنامه‌های Blazor Server قبلی، فایل Host.cshtml_ را به صورت زیر ویرایش کرد تا pre-rendering فعال شود:
<component type="typeof(App)" render-mode="ServerPrerendered" />
مشکلی که در این حالت بروز می‌کرد این بود که متد OnInitializedAsync یک کامپوننت، دوبار فراخوانی می‌شد؛ یکبار در زمان pre-rendering در سمت سرور، تا HTML استاتیکی برای ارائه‌ی به مرورگر کاربر تولید شود و بار دیگر در زمان فعال شدن اتصال SignalR کامپوننت و ارائه‌ی نهایی تعاملی آن. به همین جهت، کار سنگین آغازین یک کامپوننت، دوبار انجام می‌شد که تجربه‌ی کاربری ناخوشایندی را هم به همراه داشت. برای رفع این مشکل، tag helper ویژه‌ای را به نام persist-component-state تهیه کردند که به صورت زیر به فایل host.cshtml_ اضافه می‌شد:
<body>
    <component type="typeof(App)" render-mode="WebAssemblyPrerendered" /> 
    <persist-component-state />
</body>
این tag-helper فوق است که کار درج رمزنگاری شده‌ی اطلاعات کش شده‌ی pre-rendering سمت سرور را در انتهای فایل HTML صفحه انجام می‌دهد و برای نمونه، نحوه‌ی استفاده‌ی از آن به صورت زیر است:
@page "/"

@implements IDisposable
@inject PersistentComponentState applicationState
@inject IUserRepository userRepository

@foreach(var user in users)
{
    <ShowUserInformation user="@item"></ShowUserInformation>
}

@code {
    private const string cachingKey = "something_unique";
    private List<User> users = new();
    private PersistingComponentStateSubscription persistingSubscription;
    
    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = applicationState.RegisterOnPersisting(PersistData);

        if (applicationState.TryTakeFromJson<List<User>>(cachingKey, out var restored))
        {
            users = restored;
        }
        else 
        {
            users = await userRepository.GetAllUsers();
        }
    }

    public void Dispose()
    {
        persistingSubscription.Dispose();
    }

    private Task PersistData()
    {
        applicationState.PersistAsJson(cachingKey, users);
        return Task.CompletedTask;
    }

}
توضیحات:
- ابتدا تزریق سرویس PersistentComponentState را مشاهده می‌کنید. این همان کامپوننتی است که کار کش کردن اطلاعات را مدیریت می‌کند.
- سپس فراخوانی متد RegisterOnPersisting انجام شده‌است. متدی که توسط آن ثبت می‌شود، در حین عملیات pre-rendering فراخوانی می‌شود تا اطلاعاتی را کش کند. نحوه‌ی این کش شدن را در ادامه‌ی مطلب بررسی می‌کنیم.
- سپس فراخوانی متد TryTakeFromJson رخ‌داده‌است. اگر این متد اطلاعاتی را برگرداند، یعنی pre-rendering سمت سرور پیشتر انجام شده و اطلاعاتی کش شده، برای دریافت در سمت کلاینت، وجود داشته و نیازی به مراجعه‌ی مجدد و دوباره، به بانک اطلاعاتی نیست.
- دراینجا یک قسمت Dispose را هم مشاهده می‌کنید تا اگر کاربر به صفحه‌ی دیگری مراجعه کرد، متد ثبت شده‌ی در اینجا، از لیست مواردی که باید در حین pre-rendering سریالایز شوند، حذف گردد.

اگر از این روش استفاده نشود، به علت دوبار فراخوانی شدن متد OnInitializedAsync یک کامپوننت به همراه pre-rendering، اطلاعاتی که در حین pre-rendering کامپوننت از بانک اطلاعاتی، برای تولید محتوای استاتیک صفحه در سمت سرور دریافت شده، با فعالسازی آتی تعاملی سمت کلاینت آن کامپوننت، از دست خواهد رفت و در این حالت کلاینت باید مجددا این اطلاعات را از بانک اطلاعاتی درخواست کند. بنابراین هدف از persist-component-state، حفظ داده‌ها در بین دو رندر است؛ رندر اولی که در سمت سرور انجام می‌شود و رندر دومی که متعاقبا در سمت کلاینت رخ می‌دهد.

یک نکته: به یک چنین قابلیتی در فریم‌ورک‌های دیگر «hydration» هم گفته می‌شود که در اصل یک شیء دیکشنری است که مقدار شیء حالت را در سمت سرور دریافت کرده و آن‌را در زمان فعال شدن سمت کلاینت کامپوننت، برای استفاده‌ی مجدد مهیا می‌کند.


سؤال: اطلاعات سرویس PersistentComponentState کجا ذخیره می‌شوند؟

همانطور که در مثال فوق هم مشاهده کردید، سرویس PersistentComponentState، این state را به صورت JSON ذخیره می‌کند. اما ... این اطلاعات دقیقا کجا ذخیره می‌شوند؟
برای مشاهده‌ی آن‌ها، فقط کافی است به source صفحه مراجعه کنید تا با دو مقدار مخفی رمزنگاری شده‌ی زیر در انتهای HTML صفحه مواجه شوید!
<!--Blazor-Server-Component-State:CfDJXyz->
<!--Blazor-WebAssembly-Component-State:eyJVc2Xyz->
فرمت این اطلاعات، JsonSerializer.SerializeToUtf8Bytes رمزنگاری شده‌ی توسط IDataProtectionProvider است. این هم یک روش نگهداری اطلاعات، بجای استفاده از کش مرورگر است؛ بدون نیاز به استفاده از جاوااسکریپت و کار با local storage و امثال آن.

مایکروسافت از این نوع کارها پیشتر در ASP.NET Web forms توسط ViewStateها انجام داده‌است! البته ViewStateها جهت نگهداری اطلاعات وضعیت کلاینت، به سمت سرور ارسال می‌شوند؛ اما این Component-Stateهای مخفی، فقط یکبار توسط قسمت کلاینت خوانده می‌شوند و هدف آن‌ها ارسال اطلاعاتی به سمت سرور نیست.

یک نکته: همانطور که عنوان شد، در نگارش‌های قبلی Blazor، از تگ‌هلپری به نام persist-component-state برای درج این اطلاعات در انتهای صفحه استفاده می‌کردند. استفاده از این تگ‌هلپر در Blazor 8x منسوخ شده و به صورت خودکار داده‌های تمام سرویس‌های PersistentComponentState فعالی که توسط PersistAsJson اطلاعاتی را ذخیره می‌کنند، جمع آوری و به صورت یکجا در انتهای صفحه به صورت رمزنگاری شده، درج می‌کنند.


روش خاموش کردن Pre-rendering

برای خاموش کردن pre-rendering نیاز است پارامتر هم‌نامی را به نحو زیر با false مقدار دهی کرد:
@rendermode renderMode

@code 
{
    private static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false);
}


بازنویسی مثال قسمت قبل با استفاده از سرویس PersistentComponentState

در قسمت قبل هرچند روش مواجه شدن با مشکل دوبار فراخوانی شدن متد آغازین یک کامپوننت را در سمت سرور و کلاینت بررسی کردیم، اما ... در نهایت دوبار مراجعه به بانک اطلاعاتی انجام می‌شود؛ یکبار در زمان pre-rendering و با مراجعه‌ی مستقیم به یک سرویس سمت سرور و یکبار دیگر هم در زمان فراخوانی httpClient.GetFromJsonAsync در سمت کلاینت برای دریافت اطلاعات مورد نیاز از یک Web API Endpoint. اگر بخواهیم اطلاعات لیست محصولات دریافتی سمت سرور را به سمت کلاینت منتقل کنیم و مجددا آن‌را از بانک اطلاعاتی دریافت نکنیم، راه‌حل سوم آن، استفاده از سرویس PersistentComponentState است.

در کدهای زیر، بازنویسی کامپوننت محصولات مشابه را مشاهده می‌کنید که کمی پیچیده‌تر شده‌است. اینبار لیست محصولات مشابه، در همان بار اول رندر کامپوننت نمایش داده می‌شوند و نه پس از کلیک بر روی دکمه‌ای. به همین جهت باید کار مدیریت دوبار فراخوانی متد رویدادگردان OnInitializedAsync را به درستی انجام داد. متد OnInitializedAsync یکبار در حین پیش‌رندر سمت سرور اجرا می‌شود و بار دیگر زمانیکه وب‌اسمبلی جاری در مرورگر به صورت کامل دریافت شده و فعال می‌شود؛ یعنی برای بار دوم، کدهای اجرایی آن سمت کلاینت هستند.
در این مثال با استفاده از سرویس PersistentComponentState، اطلاعات دریافت شده‌ی در حین pre-rendering ابتدایی رخ‌داده‌ی در سمت سرور، در متد OnPersistingAsync، کش شده و در حین فعال شدن وب‌اسمبلی مرتبط در سمت کلاینت، با استفاده از متد PersistentState.TryTakeFromJson، از کش خوانده می‌شوند و دیگر دوبار رفت و برگشت به بانک اطلاعاتی را شاهد نخواهیم بود.

@implements IDisposable

@inject IProductStore ProductStore
@inject PersistentComponentState PersistentState

<h3>Related products</h3>

@if (_relatedProducts == null)
{
    <p>Loading...</p>
}
else
{
    <div class="mt-3">
        @foreach (var item in _relatedProducts)
        {
            <a href="/ProductDetails/@item.Id">
                <div class="col-sm">
                    <h5 class="mt-0">@item.Title (@item.Price)</h5>
                </div>
            </a>
        }
    </div>
}

@code{

    private const string StateCachingKey = "products";
    private IList<Product>? _relatedProducts;
    private PersistingComponentStateSubscription _persistingSubscription;

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

    protected override async Task OnInitializedAsync()
    {
        _persistingSubscription = PersistentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);

        if (PersistentState.TryTakeFromJson<IList<Product>>(StateCachingKey, out var restored))
        {
            _relatedProducts = restored;
        }
        else
        {
            await Task.Delay(500); // Simulates asynchronous loading
            _relatedProducts = await ProductStore.GetRelatedProducts(ProductId);
        }
    }

    private Task OnPersistingAsync()
    {
        PersistentState.PersistAsJson(StateCachingKey, _relatedProducts);
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _persistingSubscription.Dispose();
    }

}
در این پیاده سازی هنوز هم از سرویس IProductStore استفاده می‌شود که دو نگارش سمت سرور و سمت کلاینت آن‌را در قسمت قبل تهیه کردیم. برای مثال باتوجه به اینکه کدهای فوق در حین pre-rendering در سمت سرور اجرا می‌شوند، به صورت خودکار از نگارش سمت سرور IProductStore استفاده خواهد شد.

نکته‌ی مهم: فعلا کدهای فوق فقط برای حالت بارگذاری اولیه‌ی کامپوننت درست کار می‌کنند. یعنی اگر نگارش وب‌اسمبلی کامپوننت پس از وقوع پیش‌رندر سمت سرور، در مرورگر دریافت و بارگذاری کامل شود؛ اما برای دفعات بعدی مراجعه‌ی به این صفحه با استفاده از enhanced navigation و راهبری بهبود یافته که در قسمت ششم این سری بررسی کردیم ... دیگر کار نمی‌کنند و در این حالت کش شدنی رخ نمی‌دهد که در نتیجه، شاهد دوبار رفت و برگشت به بانک اطلاعاتی خواهیم بود و اساسا استفاده‌ی از PersistentComponentState را زیر سؤال می‌برد؛ چون در حین راهبری بهبود یافته، از آن استفاده نمی‌شود (قسمت PersistentState.TryTakeFromJson آن، هیچگاه اطلاعاتی را از کش نمی‌خواند). اطلاعات بیشتر 
مطالب
تغییرات اعمال شده در C++11 قسمت دوم (auto )
variable
متغیر :
برنامه هایی که نوشته می‌شوند برای پردازش داده‌ها بکار می‌روند،یعنی اطلاعاتی را از یک ورودی میگیرند و آنها را پردازش میکنند و نتایج مورد نظر را به خروجی می‌فرستند . برای پردازش ، لازم است که داده‌ها و نتایج ابتدا در حافظه اصلی ذخیره شوند،برای این کار از متغیر استفاده میکنیم .
متغیر مکانی از حافظه ست که شامل : نام ، نوع ، مقدار و آدرس می‌باشد . وقتی متغیری را تعریف میکنیم ابتدا با توجه به نوع متغیر ، آدرسی از حافظه در نظر گرفته می‌شود،سپس به آن آدرس یک نام تعلق میگیرد. نوع متغیر بیان میکند که در آن آدرس چه نوع داده ای می‌تواند ذخیره شود و چه اعمالی روی آن می‌توان انجام داد،مقدار نیز مشخص میکند که در آن محل از حافظه چه مقداری ذخیره شده است . در ++C قبل از استفاده از متغیر باید آن را اعلان نماییم . نحوه اعلان متغیر به شکل زیر می‌باشد :

type  name  initializer ;

عبارت type نوع متغیر را مشخص میکند . نوع متغیر به کامپایلر اطلاع میدهد که این متغیر چه مقادیری می‌تواند داشته باشد و چه اعمالی می‌توان روی آن انجام داد .عبارت name نام متغیر را نشان میدهد. عبارت initializer نیز برای مقداردهی اولیه استفاده می‌شود. نوع هایی که در ویژوال استادیو 2012 ساپورت می‌شوند شامل جدول زیر می‌باشند .

چند تعریف از متغیر به شکل زیر :

int sum(0);   //  یا  int sum=0;

char ch(65);  //  ch is A

float  pi(3.14);  //  یا  float  pi = 3.14;
همانطور که مشهود می‌باشد طبق تعریف متغیر ، نوع و نام و مقدار اولیه (اختیاری) ، مشخص گردیده است . تا قبل از C++11 تعریف نوع متغیر الزامی بود در غیر این صورت با خطای کامپایلر مواجه می‌شدیم .

تغییرات اعمال شده در C++11 :  معرفی کلمه کلیدی auto

در C++11 کلمه کلیدی auto معرفی و اضافه گردید ، با استفاده از auto ، کامپایلر این توانایی را دارد که نوع متغیر را از روی مقدار دهی اولیه آن تشخیص دهد و نیازی به مشخص نمودن نوع متغیر نداریم .

int x = 3;
auto y = x;
در تعریف فوق ابتدا نوع متغیر x را int  در نظر گرفتیم و مقدار 3 را به آن نسبت دادیم . در تعریف دوم نوع متغیر را مشخص نکردیم و کامپایلر با توجه به مقدار اولیه ای که به متغیر y  نسبت دادیم ، نوع آن را مشخص میکند . چون مقدار اولیه آن x  می‌باشد و x  از نوع int  می‌باشد پس نوع متغیر y  نیز از نوع int در نظر گرفته می‌شود .
دلایلی برای استفاده از auto :
Robustness : (خوشفکری)  به طور فرض زمانی که مقدار برگشتی یک تابع را در یک متغیر ذخیره میکنید با تغییر نوع برگشتی تابع نیازی به تغییر کد (برای نوع متغیر ذخیره کننده مقدار برگشتی تابع) ندارید .
 
int  sample()
  {
      int  result(0);
      // To Do ...
      return  result;
  }

int main()
 {
      auto  result =  sample();
      // To Do ...
      return 0;
 }
و زمانی که نوع برگشتی تابع بنا به نیاز تغییر کرد
float  sample()
  {
      float  result(0.0);
      // To Do ...
      return  result;
  }

int main()
 {
      auto  result =  sample();
      // To Do ...
      return 0;
 }
همانطور که مشاهده میکنید با اینکه کد تابع و نوع برگشتی آن تغییر یافت ولی بدنه main تابع هیچ تغییری داده نشد .
 
Usability : (قابلیت استفاده)  نیازی نیست نگران نوشتن درست و تایپ صحیح نام نوع برای متغیر باشیم
flot   f(0.0) ;   //  خطای نام نوع گرفته می‌شود
auto  f(0.0);   //  نیازی به وارد نمودن نوع تایپ نیستیم
Efficiency : برنامه نویسی ما کارآمدتر خواهد بود
مهمترین استفاده از auto سادگی آن است .
استفاده از auto  بخصوص زمانی که از STL  و templates استفاده میکنیم ، بسیار کارآمد می‌باشد و بسیاری از کد را کم میکند و باعث خوانایی بهتر کد می‌شود .

  فرض کنید که نیاز به یک iterator  جهت نمایش تمام اطلاعات کانتینری از نوع mapداریم باید از کد زیر استفاده نماییم (کانتینر را map  در نظر گرفتیم) 
map<string, string> address_book;
address_book[ "Alex" ] = "example@yahoo.com";
برای تعریف یک iterator به شکل زیر عمل میکنیم .
map<string, string>::iterator itr = address_book.begin();
با استفاده از auto  کد فوق را میتوان به شکل زیر نوشت
auto itr = address_book.begin();
  (کانتینرها :(containers) : کانتینرها اشیایی هستند که اشیا دیگر را نگهداری میکنند و دارای انواع مختلفی می‌باشند به عنوان مثال , ... vector, map )
  (تکرار کننده‌ها : (iterators): تکرار کننده‌ها اشیایی هستند که اغلب آنها اشاره گرند و با استفاده از آنها میتوان محتویات کانتینرها را همانند آرایه پیمایش کرد)
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت دهم - MobX Hooks و اعمال Async در Mobx
روشی را که تا اینجا در مورد MobX بررسی کردیم، تا نگارش 5x آن‌را پوشش می‌دهد. در همین زمان، کتابخانه‌ی دیگری به نام mobx-react-lite ارائه شد که به همراه تعدادی Hook مخصوص MobX بود تا با سیستم جدید React که مبتنی بر Hooks است، سازگار شود. این امکانات در حال حاضر با خود کتابخانه‌ی mobx-react 6x یکپارچه شده و به زودی mobx-react-lite منسوخ شده اعلام می‌شود. البته روش inject/observer بررسی شده‌ی تا نگارش 5x آن، هنوز هم برقرار است و قرار نیست که به این زودی‌ها منسوخ شده اعلام شود. به همین جهت نکاتی را که در مطلب جاری بررسی می‌کنیم، به عنوان روش تکمیلی سازگار با نگارش جاری 6x آن مطرح است و در کل با هر روشی که علاقمند بودید می‌توانید با MobX کار کنید. البته باز هم توصیه شده‌است که سیستم Provider آن‌را با React Context استاندارد، جایگزین کنید؛ چون احتمال حذف آن در نگارش‌های بعدی MobX هست.

به صورت خلاصه:
- اگر فقط از کامپوننت‌های کلاسی استفاده می‌کنید، mobx-react@5 برای کار شما پاسخگو است.
- اگر از کامپوننت‌های کلاسی و همچنین کامپوننت‌های تابعی در برنامه‌ی خود استفاده می‌کنید، mobx-react@6 به همراه mobx-react-lite نیز ارائه می‌شود و هر دو روش را با هم پوشش می‌دهد.
- اگر فقط از کامپوننت‌های تابعی جدید استفاده می‌کنید، هوک‌های کتابخانه‌ی کوچک mobx-react-lite برای کار شما کافی است.


معرفی useLocalStore Hook و useObserver Hook

در مطالب قبلی، روش تعریف یک کلاس مخزن حالت MobX را توسط تزئین کننده‌هایی مانند observable، computed و action بررسی کردیم. همچنین دریافتیم که تعریف یک چنین تزئین کننده‌هایی، یا نیاز به استفاده‌ی از تایپ‌اسکریپت را دارد و یا باید پروژه‌ی React را جهت تغییر کامپایلر Babel آن و فعالسازی decorators، مقداری ویرایش کرد. با useLocalStore Hook هرچند تمام روش‌های قبلی هنوز هم پشتیبانی می‌شوند، اما دیگر نیاز به استفاده‌ی از decorators نیست. useLocalStore تابعی است که یک شیء را باز می‌گرداند. هر خاصیتی از این شیء، به صورت خودکار observable درنظر گرفته می‌شود. تمام getters آن به عنوان computed properties تفسیر می‌شوند و تمام متدهای آن، action درنظر گرفته خواهند شد.
یک مثال:
import React from 'react'
import { useLocalStore, useObserver } from 'mobx-react' // 6.x

export const SmartTodo = () => {
  const todo = useLocalStore(() => ({
    title: 'Click to toggle',
    done: false,
    toggle() {
      todo.done = !todo.done
    },
    get emoji() {
      return todo.done ? '😜' : '🏃'
    },
  }))

  return useObserver(() => (
    <h3 onClick={todo.toggle}>
      {todo.title} {todo.emoji}
    </h3>
  ))
}
- در اینجا نحوه‌ی import تابع useLocalStore را از کتابخانه‌ی mobx-react نگارش 6x ملاحظه می‌کنید.
- روش استفاده‌ی از تابع useLocalStore، می‌تواند به صورت محلی (همانند اسم آن) مختص به یک کامپوننت باشد. یعنی می‌توان بجای state استاندارد React که اجازه‌ی تغییر مستقیم خواص آن‌را نمی‌دهد، از MobX State محلی ارائه شده‌ی توسط useLocalStore استفاده کرد و یا می‌توان useLocalStore را به صورت global نیز تعریف کرد که در ادامه‌ی بحث به آن می‌پردازیم.
- در مثال فوق، طول عمر شیء ایجاد شده‌ی توسط useLocalStore، محلی و محدود به طول عمر کامپوننت تابعی تعریف شده‌است.
- در اینجا شیء بازگشت داده شده‌ی توسط useLocalStore، دارای دو خاصیت title و done است. این دو خاصیت بدون نیاز به هیچ تعریف خاصی، observable در نظر گرفته می‌شوند. Fi به علاوه خاصیت getter آن به نام emoji نیز به عنوان یک خاصیت محاسباتی MobX تفسیر شده و متد toggle آن به صورت یک action پردازش می‌شود. بنابراین در حین کار با MobX Hooks دیگر نیازی به تغییر ساختار پروژه‌ی React، برای پشتیبانی از decorators نیست.
- در این مثال، return useObserver را نیز مشاهده می‌کنید. کار آن رندر مجدد کامپوننت، با تغییر یکی از خواص observable ردیابی شده‌ی توسط آن است.


امکان تعریف global state با کمک useLocalStore

نام useLocalStore از این جهت انتخاب شده‌است که مشخص کند مخزن حالت ایجاد شده‌ی توسط آن، درون یک کامپوننت به صورت محلی ایجاد می‌شود و سراسری نیست. اما این نکته به این معنا نیست که نمی‌توان مخزن حالت ایجاد شده‌ی توسط آن‌را در بین سلسه مراتب کامپوننت‌های برنامه به اشتراک گذاشت. توسط تابع useLocalStore می‌توان چندین مخزن حالت را ایجاد کرد و سپس توسط شیءای دیگر آن‌ها را یکی کرده و در آخر به کمک Context API خود React آن‌را در اختیار تمام کامپوننت‌های برنامه قرار داد.

تا نگارش MobX 5x (و همچنین پس از آن)، توسط inject@ می‌توان یک مخزن حالت را در اختیار یک کامپوننت قرار داد (مانند inject('myStore')). طراحی inject@ مربوط است به زمانیکه امکان دسترسی به Context پشت صحنه‌ی React به صورت عمومی توسط Context API آن ارائه نشده بود. به همین جهت از این پس دیگر نیازی به استفاده‌ی از آن نیست.


چگونه توسط MobX Hooks، یک مخزن حالت سراسری را ایجاد کنیم؟

برای ایجاد یک مخزن حالت سراسری با روش جدید MobX Hooks، مراحل زیر را می‌توان طی کرد:

الف) ایجاد شیء store
ابتدا متدی را مانند createStore ایجاد می‌کنیم، به نحوی که یک شیء را بازگشت دهد. این شیء همانطور که عنوان شد، خواصش، getters و متدهای آن، توسط MobX ردیابی خواهند شد (مانند const todo = useLocalStore مثال فوق) و نیازی به اعمال MobX Decorators را ندارند.
export function createStore() {
  return {
   // ...
  }
}

ب) برپایی Context
اینبار دیگر نه از شیء Provider خود MobX استفاده می‌کنیم و نه از تزئین کننده‌ی inject@ آن؛ بلکه از React Context استاندارد استفاده خواهیم کرد:
import React from 'react';
import { createStore } from './createStore';
import { useLocalStore } from 'mobx-react'; // 6.x or mobx-react-lite@1.4.0

const storeContext = React.createContext(null);

export const StoreProvider = ({ children }) => {
  const store = useLocalStore(createStore);
  return <storeContext.Provider value={store}>{children}</storeContext.Provider>;
}

export const useStore = () => {
  const store = React.useContext(storeContext);
  if (!store) {
    throw new Error('useStore must be used within a StoreProvider.');
  }
  return store
}
- در اینجا فرض شده‌است که تابع createStore که شیء store ما را ارائه می‌دهد از ماژولی به نام createStore دریافت می‌شود.
- سپس توسط React.createContext، یک شیء Context استاندارد React را ایجاد می‌کنیم؛ به نام storeContext.
- تابع کمکی StoreProvider، جایگزین شیء Provider قبلی MobX می‌شود. یعنی کارش محصور کردن کامپوننت App برنامه است تا شیء store را در اختیار سلسه مراتب کامپوننت‌های React قرار دهد. در اینجا children به همان کامپوننت‌هایی که قرار است توسط Context.Provider محصور شوند اشاره می‌کند.
- تابع کمکی useStore، جهت محصور کردن  متد React.useContext، اضافه شده‌است. می‌توانید useContext Hook را به صورت مستقیم در کامپوننت‌های تابعی فراخوانی کنید و یا می‌توانید از متد کمکی useStore بجای آن استفاده نمائید تا حجم کدهای تکراری برنامه کاهش یابد.

ج) استفاده‌ی از StoreProvider تهیه شده
اکنون با استفاده از متد StoreProvider فوق که شیء Context.Provider استاندارد React را بازگشت می‌دهد، می‌توان کامپوننت‌های بالاترین کامپوننت سلسه مراتب کامپوننت‌های برنامه را محصور کرد، تا تمام آن‌ها بتوانند به store ذخیره شده‌ی در Provider، دسترسی پیدا کنند:
export default function App() {
  return (
    <StoreProvider>
      <main>
        <Component1 />
        <Component2 />
        <Component3 />
      </main>
    </StoreProvider>
  );
}

د) استفاده از store مهیا شده در کامپوننت‌های تابعی برنامه
پس از تهیه‌ی متدی کمکی useStore که در حقیقت همان useContext Hook است، می‌توان به کمک آن در کامپوننت‌های تابعی، به store و تمام امکانات آن دسترسی پیدا کرد:
const store = useStore();
به این ترتیب دیگر نیازی به inject@ نخواهد بود.

سؤال: آیا هنوز هم می‌توان یک مخزن پیچیده‌ی متشکل از چندین کلاس را تشکیل داد؟
پاسخ: بله. برای مثال ابتدا دو کلاس جدید CounterStore و ThemeStore را به نحو متداولی، با استفاده‌ی از MobX decorators طراحی می‌کنیم (دقیقا مانند مثال قسمت قبل). سپس بجای ذکر نال، بجای پارامتر متد createContext، آن‌را با یک شیء جدید مقدار دهی می‌کنیم که هر کدام از خواص آن، به یک وهله از مخازن حالت ایجاد شده اشاره می‌کند:
export const storesContext = React.createContext({
  counterStore: new CounterStore(),
  themeStore: new ThemeStore(),
});

export const useStores = () => React.useContext(storesContext);
با این تعییر اگر در کامپوننتی از برنامه نیاز به برای مثال شیء منتسب به خاصیت counterStore را داشتیم، می‌توان به صورت زیر عمل کرد:
const { counterStore } = useStores();


چند نکته‌ی تکمیلی

نکته 1: با اشیاء MobX از Object Destructuring استفاده نکنید!

اگر بر روی اشیاء MobX از Object Destructuring استفاده کنیم، خروجی آن تبدیل به متغیرهای ساده‌ای خواهند شد که دیگر ردیابی نمی‌شوند.
برای مثال اگر counterStore مثال فوق به همراه خاصیت observable ای به نام activeUserName است، آن‌را به صورت زیر تبدیل به متغیر activeUserName نکنید؛ چون دیگر reactive نخواهد بود:
const {
    counterStore: { activeUserName },
} = useStores();
فقط بالاترین سطح مخزن را به صورت زیر توسط Object Destructuring از آن استخراج و سپس استفاده کنید:
const { counterStore } = useStores();


نکته 2: مدیریت side effects با MobX

در مورد اثرات جانبی و side effects در مطلب «قسمت 32 - React Hooks - بخش 3 - نکات ویژه‌ی برقراری ارتباط با سرور» بیشتر بحث شد. اگر یک اثر جانبی مانند تنظیم document.title، به مقدار یک خاصیت observable وابسته بود، می‌توان از متد autorun که تغییرات آن‌ها را ردیابی می‌کند، درون useEffect Hook استاندارد، استفاده کرد:
import React from 'react'
import { autorun } from 'mobx'

function useDocumentTitle(store) {
  React.useEffect(
    () =>
      autorun(() => {
        document.title = `${store.title} - ${store.sectionName}`
      }),
    [], // note empty dependencies
  )
}
در حین کار با MobX، هیچگاه نیازی به ذکر وابستگی‌های تابع useEffect نیست؛ چون اساسا وجود خارجی ندارند و توسط خود MobX مدیریت می‌شوند و به store وابسته‌اند و نه به حالت کامپوننت جاری.


نکته 4: روش فعالسازی MobX strict mode

اگر strict mode را در Mobx به روش زیر فعال کنیم:
import { configure } from "mobx";
configure({ enforceActions: true });
پس از آن باید حالت مدیریت شده‌ی توسط MobX را فقط و فقط توسط action‌های آن تغییر داد و اگر سعی در تغییر مقدار مستقیم یک خاصیت observable کنیم، استثنایی صادر خواهد شد. برای تغییر خواص observable باید آن‌ها را درون یک action قرار داد؛ تا مطابق رهنمودهای طراحی کلاس‌های MobX باشد.


نکته 3: روش انجام اعمال async در MobX

فرض کنید یک عملیات async را در یک اکشن متد کلاس حالت MobX، به صورت زیر انجام داده‌ایم و نتیجه‌ی آن به خاصیت weatherData آن کلاس که observable است، به صورت مستقیم انتساب داده شده‌است:
@action
loadWeather = city => {
  fetch(
    `https://abnormal-weather-api.herokuapp.com/cities/search?city=${city}`
  )
    .then(response => response.json())
    .then(data => {
      this.weatherData = data;
    });
};
هرچند loadWeather یک متد را ارائه می‌دهد که به صورت action معرفی شده‌است، اما هرچیزی که داخل آن قرار می‌گیرد، الزاما تحت کنترل آن نیست. برای مثال متد then، یک تابع callback جدید را فراخوانی می‌کند که اعمال آن، تحت کنترل loadWeather نیست. به همین جهت اگر strict mode را فعال کرده باشیم، عنوان می‌کند که خواص observable را باید درون یک اکشن متد تغییر داد و نه به صورت مستقیم؛ مانند this.weatherData در اینجا.

راه حل اول: تغییر خاصیت this.weatherData را به یک اکشن متد مجزا انتقال می‌دهیم:
@action setWeather = data => {
    this.weatherData = data;
};
اکنون می‌توان قسمت then را به صورت then(data => this.setWeather(data)) نوشت و خطای یاد شده برطرف می‌شود.

راه حل دوم: اگر نمی‌خواهیم یک اکشن متد جدید را تعریف کنیم، می‌توان از متد کمکی runInAction در داخل یک callback استفاده کرد:
  loadWeatherInline = city => {
    fetch(`http://jsonplaceholder.typicode.com/comments/${city}`)
      .then(response => response.json())
      .then(data => {
        runInAction(() => (this.weatherData = data));
      });
  };
runInAction یکی از متدهای قابل دریافت از mobx است.

در مورد اعمال async/await چطور؟
در اینجا هم تفاوتی نمی‌کند. هر چیزی پس از await، شبیه به حالت متد then پردازش می‌شود. به همین جهت در اینجا نیز باید از یکی از دو راه حل ارائه شده، استفاده کرد:
  loadWeatherAsync = async city => {
    const response = await fetch(
      `http://jsonplaceholder.typicode.com/comments/${city}`
    );
    const data = await response.json();
    runInAction(() => {
      this.weatherData = data;
    });
  };
مطالب
آشنایی با جنریک‌ها #2
قبل از ادامه آموزش مفاهیم جنریک، در نظر داشتن این نکته ضروری است که مطالبی که در این سری مقالات ارائه می‌شود در سطح مقدماتی است و قصد من آشنا نمودن برنامه نویسانی است که با این مفاهیم ناآشنا هستند ولی با مطالعه این مقاله می‌توانند کدهای تمیزتر و بهتری تولید کنند و همینطور این مفاهیم ساده، پایه‌ای باشد برای فراگیری سایر نکات تکمیلی و پیچیده‌تر جنریک‌ها.

در قسمت قبلی، نحوه تعریف کلاس جنریک شرح داده شد و در سری دوم اشاره‌ای به مفاهیم و نحوه پیاده سازی اینترفیس جنریک می‌پردازیم.
مفهوم اینترفیس جنریک همانند مفهوم اینترفیس در دات نت است. با این تفاوت که برای آن‌ها یک نوع عمومی تعریف می‌شود و نوع آن‌ها در زمان اجرا تعیین خواهد شد و کلاس بر اساس نوع اینترفیس، اینترفیس را پیاده سازی می‌کند. برای درک بهتر به نحوه تعریف اینترفیس جنریک زیر دقت کنید:
public interface IBinaryOperations<T>
{
   T Add(T arg1, T arg2);
   T Subtract(T arg1, T arg2);
   T Multiply(T arg1, T arg2);
   T Divide(T arg1, T arg2);
}
در کد بالا اینترفیسی از نوع جنریک تعریف شده است که دارای چهار متد با چهار خروجی و پارامترهای چنریک می‌باشد که نوع خروجی‌ها و نوع پارامترهای ورودی در زمان استفاده از اینترفیس تعیین می‌شوند که البته در بالا بطور خاص بیان شده است. اینترفیسی داریم که دو ورودی از هر نوعی دریافت می‌کند و چهار عملی اصلی را بر روی آن‌ها انجام داده و خروجی آن‌ها را از همان نوع پارامتر ورودی تولید می‌کند. (بجای اینترفیس‌های مختلف عملیات چهار عمل اصلی برای هر نوع داده (data type)، یک اینترفیس کلی برای تمام data typeها)
در کلاس زیر نحوه پیاده سازی اینترفیس از نوع int را مشاهده می‌کنید که چهار عملی اصلی را برروی داده هایی از نوع int انجام می‌شود و چهار خروجی از نوع int تولید می‌شود.
public class BasicMath : IBinaryOperations<int>
{
   public int Add(int arg1, int arg2)
   { return arg1 + arg2; }
 
   public int Subtract(int arg1, int arg2)
   { return arg1 - arg2; }
 
   public int Multiply(int arg1, int arg2)
   { return arg1 * arg2; }
 
   public int Divide(int arg1, int arg2)
   { return arg1 / arg2; }
}
بعد از پیاده سازی اینترفیس حال نوبت به استفاده از کلاس می‌رسد که زیر نیز نحوه استفاده از کلاس نمایش داده شده است:
static void Main(string[] args)
{
   Console.WriteLine("***** Generic Interfaces *****\n");
   BasicMath m = new BasicMath();
   Console.WriteLine("1 + 1 = {0}", m.Add(1, 1));
   Console.ReadLine();
}
و در صورتیکه بخواهید کلاسی چهار عمل اصلی را بر روی نوع داده double انجام دهد کافیست کلاسی اینترفیس نوع double را پیاده سازی کرده باشد. مانند کد زیر:
public class BasicMath : IBinaryOperations<double>
{
   public double Add(double arg1, double arg2)
   { return arg1 + arg2; }
   ...
}
برداشتی آزاد از این مقاله.