مطالب
Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایه‌ی اعتبارسنجی و احراز هویت
در این قسمت می‌خواهیم پایه‌ی اعتبارسنجی و احراز هویت سمت سرور برنامه‌ی کلاینت Blazor WASM را بر اساس JWT یکپارچه با ASP.NET Core Identity تامین کنیم. اگر با JWT آشنایی ندارید، نیاز است مطالب زیر را در ابتدا مطالعه کنید:
- «معرفی JSON Web Token»

توسعه‌ی IdentityUser

در قسمت‌های 21 تا 23، روش نصب و یکپارچگی ASP.NET Core Identity را با یک برنامه‌ی Blazor Server بررسی کردیم. در پروژه‌ی Web API جاری هم از قصد داریم از ASP.NET Core Identity استفاده کنیم؛ البته بدون نصب UI پیش‌فرض آن. به همین جهت فقط از ApplicationDbContext آن برنامه که از IdentityDbContext مشتق شده و همچنین قسمتی از تنظیمات سرویس‌های ابتدایی آن که در قسمت قبل بررسی کردیم، در اینجا استفاده خواهیم کرد.
IdentityUser پیش‌فرض که معرف موجودیت کاربران یک سیستم مبتنی بر ASP.NET Core Identity است، برای ثبت نام یک کاربر، فقط به ایمیل و کلمه‌ی عبور او نیاز دارد که نمونه‌ای از آن‌را در حین معرفی «ثبت کاربر ادمین Identity» بررسی کردیم. اکنون می‌خواهیم این موجودیت پیش‌فرض را توسعه داده و برای مثال نام کاربر را نیز به آن اضافه کنیم. برای اینکار فایل جدید BlazorServer\BlazorServer.Entities\ApplicationUser .cs را به پروژه‌ی Entities با محتوای زیر اضافه می‌کنیم:
using Microsoft.AspNetCore.Identity;

namespace BlazorServer.Entities
{
    public class ApplicationUser : IdentityUser
    {
        public string Name { get; set; }
    }
}
برای توسعه‌ی IdentityUser پیش‌فرض، فقط کافی است از آن ارث‌بری کرده و خاصیت جدیدی را به خواص موجود آن اضافه کنیم. البته برای شناسایی IdentityUser، نیاز است بسته‌ی نیوگت Microsoft.AspNetCore.Identity.EntityFrameworkCore را نیز در این پروژه نصب کرد.
اکنون که یک ApplicationUser سفارشی را ایجاد کردیم، نیازی نیست تا DbSet خاص آن‌را به ApplicationDbContext برنامه اضافه کنیم. برای معرفی آن به برنامه ابتدا باید به فایل BlazorServer\BlazorServer.DataAccess\ApplicationDbContext.cs مراجعه کرده و نوع IdentityUser را به IdentityDbContext، از طریق آرگومان جنریکی که می‌پذیرد، معرفی کنیم:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
زمانیکه این آرگومان جنریک قید نشود، از همان نوع IdentityUser پیش‌فرض خودش استفاده می‌کند.
پس از این تغییر، در فایل BlazorWasm\BlazorWasm.WebApi\Startup.cs نیز باید ApplicationUser را به عنوان نوع جدید کاربران، معرفی کرد:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
  
            // ...

پس از این تغییرات، باید از طریق خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شد و دستورات زیر را جهت ایجاد و اعمال Migrations متناظر با تغییرات فوق، اجرا کرد. چون در این دستورات اینبار پروژه‌ی آغازین، به پروژه‌ی Web API اشاره می‌کند، باید بسته‌ی نیوگت Microsoft.EntityFrameworkCore.Design را نیز به پروژه‌ی آغازین اضافه کرد، تا بتوان آن‌ها را با موفقیت به پایان رساند:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddNameToAppUser --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext


ایجاد مدل‌های ثبت نام

در ادامه می‌خواهیم کنترلری را ایجاد کنیم که کار ثبت نام و لاگین را مدیریت می‌کند. برای این منظور باید بتوان از کاربر، اطلاعاتی مانند نام کاربری و کلمه‌ی عبور او را دریافت کرد و پس از پایان عملیات نیز نتیجه‌ی آن‌را بازگشت داد. به همین جهت دو مدل زیر را جهت مدیریت قسمت ثبت نام، به پروژه‌ی BlazorServer.Models اضافه می‌کنیم:
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class UserRequestDTO
    {
        [Required(ErrorMessage = "Name is required")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Email is required")]
        [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
                ErrorMessage = "Invalid email address")]
        public string Email { get; set; }

        public string PhoneNo { get; set; }

        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Required(ErrorMessage = "Confirm password is required")]
        [DataType(DataType.Password)]
        [Compare("Password", ErrorMessage = "Password and confirm password is not matched")]
        public string ConfirmPassword { get; set; }
    }
}
و مدل پاسخ عملیات ثبت نام:
    public class RegistrationResponseDTO
    {
        public bool IsRegistrationSuccessful { get; set; }

        public IEnumerable<string> Errors { get; set; }
    }


ایجاد و تکمیل کنترلر Account، جهت ثبت نام کاربران

در ادامه نیاز داریم تا جهت ارائه‌ی امکانات اعتبارسنجی و احراز هویت کاربران، کنترلر جدید Account را به پروژه‌ی Web API اضافه کنیم:
using System;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Linq;
using BlazorServer.Common;

namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class AccountController : ControllerBase
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public AccountController(SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager)
        {
            _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> SignUp([FromBody] UserRequestDTO userRequestDTO)
        {
            var user = new ApplicationUser
            {
                UserName = userRequestDTO.Email,
                Email = userRequestDTO.Email,
                Name = userRequestDTO.Name,
                PhoneNumber = userRequestDTO.PhoneNo,
                EmailConfirmed = true
            };
            var result = await _userManager.CreateAsync(user, userRequestDTO.Password);
            if (!result.Succeeded)
            {
                var errors = result.Errors.Select(e => e.Description);
                return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
            }

            var roleResult = await _userManager.AddToRoleAsync(user, ConstantRoles.Customer);
            if (!roleResult.Succeeded)
            {
                var errors = result.Errors.Select(e => e.Description);
                return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
            }

            return StatusCode(201); // Created
        }
    }
}
- در اینجا اولین اکشن متد کنترلر Account را مشاهده می‌کنید که کار ثبت نام یک کاربر را انجام می‌دهد. نمونه‌‌ای از این کدها پیشتر در قسمت 23 این سری، زمانیکه کاربر جدیدی را با نقش ادمین تعریف کردیم، مشاهده کرده‌اید.
- در تعریف ابتدایی این کنترلر، ویژگی‌های زیر ذکر شده‌اند:
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
می‌خواهیم مسیریابی آن با api/ شروع شود و به صورت خودکار بر اساس نام کنترلر و نام اکشن متدها، تعیین گردد. همچنین نمی‌خواهیم مدام کدهای بررسی معتبر بودن ModelState را در کنترلرها قرار دهیم. به همین جهت از ویژگی ApiController استفاده شده تا اینکار را به صورت خودکار انجام دهد. قرار دادن فیلتر Authorize بر روی یک کنترلر سبب می‌شود تا تمام اکشن متدهای آن به کاربران اعتبارسنجی شده محدود شوند؛ مگر اینکه عکس آن به صورت صریح توسط فیلتر AllowAnonymous مشخص گردد. نمونه‌ی آن‌را در اکشن متد عمومی SignUp در اینجا مشاهده می‌کنید.

تا اینجا اگر برنامه را اجرا کنیم، می‌توان با استفاده از Swagger UI، آن‌را آزمایش کرد:


که با اجرای آن، برای نمونه به خروجی زیر می‌رسیم:


که عنوان می‌کند کلمه‌ی عبور باید حداقل دارای یک عدد و یک حرف بزرگ باشد. پس از اصلاح آن، status-code=201 را دریافت خواهیم کرد.
و اگر سعی کنیم همین کاربر را مجددا ثبت نام کنیم، با خطای زیر مواجه خواهیم شد:



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

در پروژه‌ی Web API، از UI پیش‌فرض ASP.NET Core Identity استفاده نمی‌کنیم. به همین جهت نیاز است مدل‌های قسمت لاگین را به صورت زیر تعریف کنیم:
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class AuthenticationDTO
    {
        [Required(ErrorMessage = "UserName is required")]
        [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
            ErrorMessage = "Invalid email address")]
        public string UserName { get; set; }

        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
به همراه مدل پاسخ ارائه شده‌ی حاصل از عملیات لاگین:
using System.Collections.Generic;

namespace BlazorServer.Models
{
    public class AuthenticationResponseDTO
    {
        public bool IsAuthSuccessful { get; set; }

        public string ErrorMessage { get; set; }

        public string Token { get; set; }

        public UserDTO UserDTO { get; set; }
    }

    public class UserDTO
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string PhoneNo { get; set; }
    }
}
که در اینجا اگر عملیات لاگین با موفقیت به پایان برسد، یک توکن، به همراه اطلاعاتی از کاربر، به سمت کلاینت ارسال خواهد شد؛ در غیر اینصورت، متن خطای مرتبط بازگشت داده می‌شود.


ایجاد مدل مشخصات تولید JSON Web Token

پس از لاگین موفق، نیاز است یک JWT را تولید کرد و در اختیار کلاینت قرار داد. مشخصات ابتدایی تولید این توکن، توسط مدل زیر تعریف می‌شود:
namespace BlazorServer.Models
{
    public class BearerTokensOptions
    {
        public string Key { set; get; }

        public string Issuer { set; get; }

        public string Audience { set; get; }

        public int AccessTokenExpirationMinutes { set; get; }
    }
}
که شامل کلید امضای توکن، مخاطبین، صادر کننده و مدت زمان اعتبار آن به دقیقه‌است و به صورت زیر در فایل BlazorWasm\BlazorWasm.WebApi\appsettings.json تعریف می‌شود:
{
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "https://localhost:5001/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 20
  }
}
پس از این تعاریف، جهت دسترسی به مقادیر آن توسط سیستم تزریق وابستگی‌ها، مدخل آن‌را به صورت زیر به کلاس آغازین Web API اضافه می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions<BearerTokensOptions>().Bind(Configuration.GetSection("BearerTokens"));
            // ...

ایجاد سرویسی برای تولید JSON Web Token

سرویس زیر به کمک سرویس توکار UserManager مخصوص Identity و مشخصات ابتدایی توکنی که معرفی کردیم، کار تولید یک JWT را انجام می‌دهد:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace BlazorServer.Services
{

    public interface ITokenFactoryService
    {
        Task<string> CreateJwtTokensAsync(ApplicationUser user);
    }

    public class TokenFactoryService : ITokenFactoryService
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly BearerTokensOptions _configuration;

        public TokenFactoryService(
                UserManager<ApplicationUser> userManager,
                IOptionsSnapshot<BearerTokensOptions> bearerTokensOptions)
        {
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            if (bearerTokensOptions is null)
            {
                throw new ArgumentNullException(nameof(bearerTokensOptions));
            }
            _configuration = bearerTokensOptions.Value;
        }

        public async Task<string> CreateJwtTokensAsync(ApplicationUser user)
        {
            var signingCredentials = new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Key)),
                SecurityAlgorithms.HmacSha256);
            var claims = await getClaimsAsync(user);
            var now = DateTime.UtcNow;
            var tokenOptions = new JwtSecurityToken(
                issuer: _configuration.Issuer,
                audience: _configuration.Audience,
                claims: claims,
                notBefore: now,
                expires: now.AddMinutes(_configuration.AccessTokenExpirationMinutes),
                signingCredentials: signingCredentials);
            return new JwtSecurityTokenHandler().WriteToken(tokenOptions);
        }

        private async Task<List<Claim>> getClaimsAsync(ApplicationUser user)
        {
            string issuer = _configuration.Issuer;
            var claims = new List<Claim>
            {
                // Issuer
                new Claim(JwtRegisteredClaimNames.Iss, issuer, ClaimValueTypes.String, issuer),
                // Issued at
                new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, issuer),
                new Claim(ClaimTypes.Name, user.Email, ClaimValueTypes.String, issuer),
                new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String, issuer),
                new Claim("Id", user.Id, ClaimValueTypes.String, issuer),
                new Claim("DisplayName", user.Name, ClaimValueTypes.String, issuer),
            };

            var roles = await _userManager.GetRolesAsync(user);
            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role, ClaimValueTypes.String, issuer));
            }
            return claims;
        }
    }
}
کار افزودن نقش‌های یک کاربر به توکن او، به کمک متد userManager.GetRolesAsync انجام شده‌است. نمونه‌ای از این سرویس را پیشتر در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» مشاهده کرده‌اید. البته در آنجا از سیستم Identity برای تامین نقش‌های کاربران استفاده نمی‌شود و مستقل از آن عمل می‌کند.

در آخر، این سرویس را به صورت زیر به لیست سرویس‌های ثبت شده‌ی پروژه‌ی Web API، اضافه می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ITokenFactoryService, TokenFactoryService>();
            // ...


تکمیل کنترلر Account جهت لاگین کاربران

پس از ثبت نام کاربران، اکنون می‌خواهیم امکان لاگین آن‌ها را نیز فراهم کنیم:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class AccountController : ControllerBase
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly ITokenFactoryService _tokenFactoryService;

        public AccountController(
            SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager,
            ITokenFactoryService tokenFactoryService)
        {
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
            _tokenFactoryService = tokenFactoryService;
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> SignIn([FromBody] AuthenticationDTO authenticationDTO)
        {
            var result = await _signInManager.PasswordSignInAsync(
                    authenticationDTO.UserName, authenticationDTO.Password,
                    isPersistent: false, lockoutOnFailure: false);
            if (!result.Succeeded)
            {
                return Unauthorized(new AuthenticationResponseDTO
                {
                    IsAuthSuccessful = false,
                    ErrorMessage = "Invalid Authentication"
                });
            }

            var user = await _userManager.FindByNameAsync(authenticationDTO.UserName);
            if (user == null)
            {
                return Unauthorized(new AuthenticationResponseDTO
                {
                    IsAuthSuccessful = false,
                    ErrorMessage = "Invalid Authentication"
                });
            }

            var token = await _tokenFactoryService.CreateJwtTokensAsync(user);
            return Ok(new AuthenticationResponseDTO
            {
                IsAuthSuccessful = true,
                Token = token,
                UserDTO = new UserDTO
                {
                    Name = user.Name,
                    Id = user.Id,
                    Email = user.Email,
                    PhoneNo = user.PhoneNumber
                }
            });
        }
    }
}
در اکشن متد جدید لاگین، اگر عملیات ورود به سیستم با موفقیت انجام شود، با استفاده از سرویس Token Factory که آن‌را پیشتر ایجاد کردیم، توکن مخصوصی را به همراه اطلاعاتی از کاربر، به سمت برنامه‌ی کلاینت بازگشت می‌دهیم.

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


و پس از اجرای درخواست، به خروجی زیر می‌رسیم:


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


تنظیم Web API برای پذیرش و پردازش JWT ها

تا اینجا پس از لاگین، یک JWT را در اختیار کلاینت قرار می‌دهیم. اما اگر کلاینت این JWT را به سمت سرور ارسال کند، اتفاق خاصی رخ نخواهد داد و توسط آن، شیء User قابل دسترسی در یک اکشن متد، به صورت خودکار تشکیل نمی‌شود. برای رفع این مشکل، ابتدا بسته‌ی جدید نیوگت Microsoft.AspNetCore.Authentication.JwtBearer را به پروژه‌ی Web API اضافه می‌کنیم، سپس به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            var bearerTokensSection = Configuration.GetSection("BearerTokens");
            services.AddOptions<BearerTokensOptions>().Bind(bearerTokensSection);
            // ...

            var apiSettings = bearerTokensSection.Get<BearerTokensOptions>();
            var key = Encoding.UTF8.GetBytes(apiSettings.Key);
            services.AddAuthentication(opt =>
            {
                opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidAudience = apiSettings.Audience,
                    ValidIssuer = apiSettings.Issuer,
                    ClockSkew = TimeSpan.Zero,
                    ValidateLifetime = true
                };
            });

            // ...
در اینجا در ابتدا اعتبارسنجی از نوع Jwt تعریف شده‌است و سپس پردازش کننده و وفق دهنده‌ی آن‌را به سیستم اضافه کرده‌ایم تا توکن‌های دریافتی از هدرهای درخواست‌های رسیده را به صورت خودکار پردازش و تبدیل به Claims شیء User یک اکشن متد کند.


افزودن JWT به تنظیمات Swagger

هر کدام از اکشن متدهای کنترلرهای Web API برنامه که مزین به فیلتر Authorize باشد‌، در Swagger UI با یک قفل نمایش داده می‌شود. در این حالت می‌توان این UI را به نحو زیر سفارشی سازی کرد تا بتواند JWT را دریافت و به سمت سرور ارسال کند:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "BlazorWasm.WebApi", Version = "v1" });
                c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    In = ParameterLocation.Header,
                    Description = "Please enter the token in the field",
                    Name = "Authorization",
                    Type = SecuritySchemeType.ApiKey
                });
                c.AddSecurityRequirement(new OpenApiSecurityRequirement {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"
                            }
                        },
                        new string[] { }
                    }
                });
            });
        }

        // ...


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-25.zip
نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
امکان تبدیل رخدادهای توکار مرورگرها به دایرکتیوهای Blazor در Blazor6x

یکسری دایرکتیو مانند onclick@ و امثال آن، از پیش در Blazor تعریف شده‌اند که امکان مدیریت رویدادهای جاوااسکریپتی را در کدهای سی‌شارپ میسر می‌کنند. اما تعداد این‌ها زیاد نیست. برای مثال تعداد رویدادهای قابل تعریف و پشتیبانی شده‌ی توسط مرورگرها قابل ملاحظه‌است. در Blazor 6x روشی جهت دسترسی ساده‌تر به این رویدادها ارائه شده‌است که شامل این مراحل است. برای نمونه فرض کنید می‌خواهیم به رویداد paste مرورگر دسترسی پیدا کنیم و یک دایرکتیو سفارشی oncustompaste@ را برای آن تهیه کنیم:
<input @oncustompaste="HandleCustomPaste" />
برای اینکار در ابتدا قطعه کد زیر را پس از blazor.webassembly.js در فایل index.html ثبت می‌کنیم (یا می‌توان از روش export function afterStarted که در بالا عنوان شد هم استفاده کرد):
<script> 
    Blazor.registerCustomEventType('custompaste', { 
        browserEventName: 'paste', 
        createEventArgs: event => { 
            // This example only deals with pasting text, but you could use arbitrary JavaScript APIs 
            // to deal with users pasting other types of data, such as images 
            return { 
                eventTimestamp: new Date(), 
                pastedData: event.clipboardData.getData('text') 
            }; 
        } 
    }); 
</script>
در اینجا برای رویداد paste مرورگر، تعدادی آرگومان تهیه شده و بازگشت داده می‌شود. آرگومان اول در اینجا یک مقدار اختیاری و نمایشی‌است و آرگومان دوم به شیء رویداد paste، دسترسی یافته و متن آن‌را بازگشت می‌دهد.
پس از اینکار، معادل دو پارامتر بازگشت داده شده را به صورت زیر در کدهای سی‌شارپ تهیه می‌کنیم:
namespace BlazorCustomEventArgs.CustomEvents 
{ 
    [EventHandler("oncustompaste", typeof(CustomPasteEventArgs), enableStopPropagation: true, enablePreventDefault: true)] 
    public static class EventHandlers 
    { 
        // This static class doesn't need to contain any members. It's just a place where we can put 
        // [EventHandler] attributes to configure event types on the Razor compiler. This affects the 
        // compiler output as well as code completions in the editor. 
    } 
 
    public class CustomPasteEventArgs : EventArgs 
    { 
        // Data for these properties will be supplied by custom JavaScript logic 
        public DateTime EventTimestamp { get; set; } 
        public string PastedData { get; set; } 
    } 
}
ابتدا از EventArgs ارث‌بری شده و معادل تاریخ و متن بازگشت داده شده، تبدیل به یک EventArgs سفارشی می‌شود. سپس نوع آن، به ویژگی EventHandler ای که بالای یک کلاس استاتیک خالی قرار گرفته شده، ارسال می‌شود. اینکار صرفا جهت اطلاع کامپایلر صورت می‌گیرد.
یک نکته: در اینجا نام oncustompaste به همان نام custompaste کدهای جاوااسکریپتی اشاره می‌کند. نام تعریف شده‌ی در قسمت سی‌شارپ، یک on در ابتدا اضافه‌تر دارد. اینکار سبب می‌شود که اکنون بتوان یک رویدادگردان oncustompaste@ سفارشی را که قابل مدیریت در کدهای سی‌شارپ است، داشت:
@page "/"

<p>Try pasting into the following text box:</p>
<input @oncustompaste="HandleCustomPaste" />
<p>@message</p>

@code {
    string message;
    void HandleCustomPaste(CustomPasteEventArgs eventArgs)
    {
        message = $"At {eventArgs.EventTimestamp.ToShortTimeString()}, you pasted: {eventArgs.PastedData}";
    }
}
مطالب
اشتباهات متداول برنامه‌نویس‌های دات نت

اشتباه 1:
استفاده از

throw ex;
بجای استفاده از
throw;
در حالت اول، تمام stack trace موجود تا نقطه‌ی فراخوانی دستور ذکر شده پاک خواهند شد، اما در حالت دوم stack trace‌ حفظ شده و دیباگ کردن کد را ساده می‌کند.

اشتباه 2:
درک اشتباه عملکرد متد replace :
string s = "Take this out";
s.Replace("this", "that"); //wrong
بجای استفاده از :
s = s.Replace("this", "that");  //correct

اگر از fxCop استفاده کنید، اینگونه خطاها را (عدم استفاده از مقدار بازگشتی) گوشزد می‌کند.

اشتباه 3:
استفاده‌ی بی دقت از متغیرهای استاتیک در یک برنامه وب. دو مثال زیر را در نظر بگیرید:
public static string GetCookieName(Cookie c)
{
return c.Name;
}

static List<string> cookieList = new List<string>();
public static void AddToCookieList(Cookie c)
{
cookieList.Add(c.Name);

}

برنامه‌های وب ذاتا چند ریسمانی هستند و زمانیکه یک متغیر را از نوع استاتیک تعریف می‌کنید، این متغیر، هنگام مراجعه‌ی کاربران بین آن‌ها به اشتراک گذاشته می‌شود و امکان تخریب یا استفاده‌ی ناصحیح از مقادیر آن‌ها وجود خواهد داشت. در حالت اول نیازی به مباحث همزمانی و قفل کردن منابع نیست زیرا متغیری که در متد استفاده می‌شود، thread safe است اما cookieList در مثال دوم خیر و حتما هنگام استفاده از آن باید مباحث قفل کردن منابع را درنظر داشت. (حتی اگر برنامه‌ی شما از نوع وبی هم نیست اما چند ریسمانی است این مطلب باز هم صادق می‌باشد)


اشتباه 4:
مقابله با خطاها به شکلی نادرست: (اصطلاحا خفه کردن خطاها!)
 try
{
//something
File.Delete(blah);
}
catch{}
در این حالت اگر خطایی رخ دهد کاربر متوجه نخواهد شد دقیقا مشکلی وجود داشته یا خیر و علت آن چیست.
هرچند گاهی از اوقات اگر خطای حاصل برای ما اهمیتی نداشت می‌توان از آن استفاده نمود، در غیراینصورت باید حتما از این روش پرهیز کرد.

اشتباه 5:
ارائه‌ی برنامه‌های ASP.Net با گزینه‌ی پیش فرض Debug=true در web.config که پیشتر در مورد آن در این سایت بحث شده است.


اشتباه 6:
عدم استفاده از امکانات ویژه‌ی دات نت فریم ورک هنگام کار با رشته‌ها:
string s = "This ";
s += "is ";
s += "not ";
s += "the ";
s += "best ";
s += "way.";

StringBuilder sb = new StringBuilder();
sb.Append("This ");
sb.Append("is ");
sb.Append("much ");
sb.Append("better. ");

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

اشتباه 7:
عدم استفاده از عبارت using هنگام استفاده از اشیایی از نوع Idisposable . مثالی در این مورد.

using (StreamReader reader=new StreamReader(file))
{
//your code here

}

اشتباه 8:
فراموش کردن بررسی نال بودن یک شیء هنگام استفاده از آن.
string val = Session["xyz"].ToString();

این نوع کد نویسی یکی از اشتباهات متداول تمامی تازه واردان به ASP.Net است. حتما باید پیش از استفاده از متد ToString بررسی شود که آیا این سشن نال است یا نه. در غیراینصورت حاصل کار فقط یک exception خواهد بود. (استفاده از افزونه‌ی ری شارپر در این موارد کمک بزرگی است، زیرا به محض قرار گرفتن مکان نما روی شیءایی که احتمال نال بودن آن میسر است، یک راهنما را به شما ارائه خواهد کرد)

اشتباه 9:
بازگشت دادن یک property عمومی از نوع لیست‌های جنریک.
با توجه به اینکه این نوع لیست‌ها فقط خواندنی نیستند و امکان دستکاری اطلاعات آن توسط فراخوان وجود دارد، توصیه می‌شود از نوع جنریک IEnumerable استفاده شود. همچنین توصیه شده است هنگام انتخاب نوع پارامترهای ورودی یک متد نیز به این مورد دقت شود.

مطالب
آموزش Knockout.Js #2
در پست قبلی با مفاهیم و ویژگی‌های کلی KO آشنا شدید. KO از الگوی طراحی MVVM استفاده می‌کند. از آن جا که یکی از پیش نیاز‌های KO آشنایی اولیه با مفاهیم View و Model است نیاز به توضیح در این موارد نیست اما اگر به هر دلیلی با این مفاهیم آشنایی ندارید می‌توانید از اینجا شروع کنید. اما درباره ViewModel که کمی مفهوم متفاوتی دارد، این نکته قابل ذکر است که KO از ViewModel برای ارتباط مستقیم بین View و Model استفاده می‌کند، چیزی شبیه به منطق MVC با این تفاوت که ViewModel به جای Controller قرار خواهد گرفت.

ابتدا باید به شرح برخی مفاهیم در KO بپردازم:
»Observable(قابل مشاهده کردن تغییرات)
KO از Observable برای ردیابی و مشاهده تغییرات خواص ViewModel استفاده می‌کند. در واقع Observable دقیقا شبیه به متغیر‌ها در JavaScript عمل می‌کنند با این تفاوت که به KO اجازه می‌دهند که تغییرات این خواص را پیگیری کند و این تغییرات را به بخش‌های مرتبط View اعمال نماید. اما سوال این است که KO چگونه متوجه می‌شود که این تغییرات بر کدام قسمت در View تاثیر خواهند داشت؟ جواب این سوال در مفهوم Binding است.
»Binding
برای اتصال بخش‌های مختلف View به Observable‌ها باید از binding(مقید سازی) استفاده کنیم. بدون عملیات binding، امکان اعمال تغییرات Observable‌ها بر روی عناصر HTML امکان پذیر نیست.
برای مثال در شکل زیر یکی از خواص ViewModel را به View متناظر مقید شده است.

با کمی دقت در شکل بالا این نکته به دست می‌آید که می‌توان در یک ViewModel، فقط خواص مورد نظر را به عناصر Html مقید کرد.

دانلود فایل‌های مورد نیاز

فایل‌های مورد نیاز برای KO رو می‌توانید از اینجا دانلود نمایید و به پروژه اضافه کنید. به صورت پیش فرض فایل‌های مورد نیاز KO، در پروژه‌های MVC 4 وجود دارد و نیاز به دانلود آن‌ها نیست و شما باید فقط مراحل BundleConfig را انجام دهید.

تعریف ViewModel

برای تعریف ViewModel و پیاده سازی مراحل Observable و binding باید به صورت زیر عمل نمایید:

<html lang='en'>
<head>
<title>Hello, Knockout.js</title>
<meta charset='utf-8' />
<link rel='stylesheet' href='style.css' />
</head>
<body>
<h1>Hello, Knockout.js</h1>
<script type='text/javascript' src='knockout-2.1.0.js'>   
      <script type='text/javascript'>
             var personViewModel = {
                  firstName: "Masoud",
                  lastName: "Pakdel"
                };
                 ko.applyBindings(personViewModel);
       </script>
</script>
</body>
</html>
مشاهده می‌کنید که ابتدا یک ViewModel به نام person ایجاد کردم همراه با دو خاصیت به نام‌های firstName و lastName. تابع applyBinding برای KO بدین معنی است که این آبجکت به عنوان یک ViewModel در این صفحه مورد استفاده قرار خواهد گرفت. اما برای مشاهده تغییرات باید یک عنصر HTML را یه این ViewModel مقید(bind) کنیم.

مقید سازی عناصر HTML

برای مقید سازی عناصر HTML به ViewModel‌ها باید از data-bind attribute استفاده نماییم. برای مثال:
<p><span data-bind='text: firstName'></span>'s Shopping Cart</p>
اگر به data-bind در تگ span بالا توجه کنید خواهید دید که مقدار text در این تگ را به خاصیت firstName در viewModel این صفحه bind شده است. تا اینجا KO می‌داند که چه عنصر از DOM به کدام خاصیت از ViewModel مقید شده است اما هنوز دستور ردیابی تغییرات(Observable) را برای KO تعیین نکردیم.

چگونه خواص را Observable کنیم
در پروژه‌های WPF، فقط در صورتی تغییرات خواص یک کلاس ردیابی می‌شوند که اولا کلاس اینترفیس INotifyPropertyChanged را پیاده سازی کرده باشد ثانیا، در متد set این خواص، متد OnPropertyChanged(البته این متد می‌تواند هر نام دیگری نیز داشته باشد) صدا زده شده باشد. نکته مهم و اساسی در KO نیز همین است که برای اینکه KO بتواند تغییرات هر خاصیت را مشاهده کند حتما خواص مورد نظر  باید Observable  شوند. برای این کار کافیست به صورت عمل کنید:
var personViewModel = {
  firstName: ko.observable("Masoud"),
  lastName: ko.observable("Pakdel")
};
مزیت اصلی برای اینکه حتما خواص مورد نظرتان  Observable شوند این است که، در صورتی که مایل نباشید تغییرات یک خاصیت  بر روی View اعمال شود کافیست از دستور بالا استفاده نکنید. درست مثل اینکه هرگز مقدار آن تغییر نکرده است.

پیاده سازی متد‌های get و set
همان طور که متوجه شدید، Observable‌ها متغیر نیستند بلکه تابع هستند در نتیجه برای دستیابی به مقدار یک observable کافیست آن را بدون پارامتر ورودی صدا بزنیم و برای تغییر در مقدار آن باید همان تابع را با مقدار جدید صدا بزنیم. برای مثال:
personViewModel.firstName() // Get
personViewModel.firstName("Masoud") // Set
البته این نکته را هم متذکر شوم که در ViewModel‌های خود می‌توانید توابع سفارشی مورد نیاز را بنویسید و از آن‌ها در جای مناسب استفاده نماید(شبیه به مفاهیم Command‌ها در WPF)
مقید سازی تعاملی
اگر با WPF آشنایی دارید می‌دانید که در این گونه پروژه‌ها می‌توان رویداد‌های مورد نظر را به Command‌های خاص در ViewModel مقید کرد. در KO نیز این امر به آسانی امکان پذیر است که به آن Interactive Bindings می‌گویند. فقط کافیست در data-bind attribute  از نام رویداد استفاده نماییم. مثال:
ایتدا بک ViewModel به صورت زیر خواهیم داشت:
function PersonViewModel() {
   this.firstName = ko.observable("Masoud");
   this.lastName = ko.observable("Pakdel");
   this.clickMe= function() {
    alert("this is test!");
  };
};
تنها نکته قابل ذکر تعریف تابع سفارشی به نام clickMe است که به نوعی معادل Command مورد نظر ما در WPF است.  در عنصر HTML مورد نظر که در این جا button است باید data-binding به صورت زیر باشد:
<button data-bind='click: clickMe'>Click Me...</button>
در نتیجه بعد از کلیک بر روی button بالا تابع مورد نظر در viewModel اجرا خواهد شد.
پس به صورت خلاصه:
  • ابتدا ViewModel مورد نظر را ایجاد نمایید؛
  • سپس با استفاده از data-bind عملیات مقید سازی بین View و ViewModel را انجام دهید
  • در نهایت با استفاده از Obsevable تغییرات خواص مورد نظر را ردیابی نمایید.

ادامه دارد...

 
اشتراک‌ها
تولید شرط های پویا توسط کتابخانه LambdaExpressionBuilder

A library that provides a simple way to create lambda expressions to filter lists and database queries. 

var filter = new Filter<Person>();
filter.By("Id", Operation.Between, 2, 4,  Connector.And);
filter.By("Contacts[Value]", Operation.EndsWith, "@email.com", default(string), Connector.And);
filter.By("Birth.Country", Operation.IsNotNull, default(string), default(string),  Connector.Or);
filter.By("Name", Operation.Contains, " John");
var people = People.Where(filter);

//or like this...

var filter = new Filter<Person>();
filter.By("Id", Operation.Between, 2, 4)
      .And.By("Birth.Country", Operation.IsNotNull)
      .And.By("Contacts[Value]", Operation.EndsWith, "@email.com")
      .Or.By("Name", Operation.Contains, " John ");
var people = People.Where(filter);

So that would generate an expression like this: 

People.Where(p => (p.Id >= 2 && p.Id <= 4)
             && (p.Birth != null && p.Birth.Country != null)
             && (p.Contacts != null && p.Contacts.Any(c => c.Value.Trim().ToLower().EndsWith("@email.com")))
             || (p.Name != null  && p.Name.Trim().ToLower().Contains("john")));

LambdaExpressionBuilder Nuget Package 

تولید شرط های پویا توسط کتابخانه LambdaExpressionBuilder
مطالب
ارسال مستقیم یک فایل PDF به چاپگر
برنامه رایگان Adobe reader یک سری خط فرمان دارد که توسط آن‌ها می‌توان فایل‌های PDF را مستقیما به چاپگر ارسال کرد. در ادامه قطعه کدی را ملاحظه خواهید کرد که انجام اینکار را کپسوله می‌کند:
using System;
using System.Diagnostics;
using System.IO;
using System.Management;
using Microsoft.Win32;

namespace PdfFilePrinter
{
    /// <summary>
    /// Executes the Adobe Reader and prints a file while suppressing the Acrobat print
    /// dialog box, then terminating the Reader.
    /// </summary>
    public class AcroPrint
    {
        /// <summary>
        /// The Adobe Reader or Adobe Acrobat path such as 'C:\Program Files\Adobe\Adobe Reader X\AcroRd32.exe'.
        /// If it's not specified, the InstalledAdobeReaderPath property value will be used.
        /// </summary>
        public string AdobeReaderPath { set; get; }

        /// <summary>
        /// Returns the default printer name.
        /// </summary>
        public string DefaultPrinterName
        {
            get
            {
                var query = new ObjectQuery("SELECT * FROM Win32_Printer");
                using (var searcher = new ManagementObjectSearcher(query))
                {
                    foreach (var mo in searcher.Get())
                    {
                        if (((bool?)mo["Default"]) ?? false)
                            return mo["Name"] as string;
                    }
                }
                return string.Empty;
            }
        }

        /// <summary>
        /// The name and path of the PDF file to print.
        /// </summary>
        public string PdfFilePath { set; get; }

        /// <summary>
        /// Name of the printer such as '\\PrintServer\HP LaserJet'.
        /// If it's not specified, the DefaultPrinterName property value will be used.
        /// </summary>
        public string PrinterName { set; get; }

        /// <summary>
        /// Returns the HKEY_CLASSES_ROOT\Software\Adobe\Acrobat\Exe value.
        /// If AcroRd32.exe does not exist, returns string.Empty
        /// </summary>
        public string InstalledAdobeReaderPath
        {
            get
            {
                var acroRd32Exe = Registry.ClassesRoot.OpenSubKey(@"Software\Adobe\Acrobat\Exe", writable: false);
                if (acroRd32Exe == null)
                    return string.Empty;

                var exePath = acroRd32Exe.GetValue(string.Empty) as string;
                if (string.IsNullOrEmpty(exePath))
                    return string.Empty;

                exePath = exePath.Trim(new[] { '"' });
                return File.Exists(exePath) ? exePath : string.Empty;
            }
        }

        /// <summary>
        /// Executes the Adobe Reader and prints a file while suppressing the Acrobat print
        /// dialog box, then terminating the Reader.
        /// </summary>
        /// <param name="timeout">The amount of time, in milliseconds, to wait for the associated process to exit. The maximum is the largest possible value of a 32-bit integer, which represents infinity to the operating system.</param>
        public void PrintPdfFile(int timeout = Int32.MaxValue)
        {
            if (!File.Exists(PdfFilePath))
                throw new ArgumentException(PdfFilePath + " does not exist.");

            var args = string.Format("/N /T \"{0}\" \"{1}\"", PdfFilePath, getPrinterName());
            var process = startAdobeProcess(args);
            if (!process.WaitForExit(timeout))
                process.Kill();
        }

        private Process startAdobeProcess(string arguments = "")
        {
            var startInfo = new ProcessStartInfo
                {
                    FileName = this.getExePath(),
                    Arguments = arguments,
                    CreateNoWindow = true,
                    ErrorDialog = false,
                    UseShellExecute = false,
                    Verb = "print"
                };

            return Process.Start(startInfo);
        }

        private string getPrinterName()
        {
            var printer = PrinterName;
            if (string.IsNullOrEmpty(printer))
                printer = DefaultPrinterName;

            if (string.IsNullOrEmpty(printer))
                throw new ArgumentException("Please set the PrinterName.");

            return printer;
        }

        private string getExePath()
        {
            var exePath = AdobeReaderPath;
            if (string.IsNullOrEmpty(exePath) || !File.Exists(exePath))
                exePath = InstalledAdobeReaderPath;

            if (string.IsNullOrEmpty(exePath))
                throw new ArgumentException("Please set the full path of the AcroRd32.exe or Acrobat.exe.");

            return exePath;
        }
    }
}

توضیحات:
استفاده ابتدایی از کلاس فوق به نحو زیر است:
            new AcroPrint 
            {
                PdfFilePath = @"D:\path\test.pdf"
            }.PrintPdfFile();
به این ترتیب فایل PDF ذکر شده به چاپگر پیش فرض سیستم ارسال می‌شود.

ملاحظات:
- کدهای فوق نیاز به ارجاعی به اسمبلی استاندارد System.Management.dll نیز دارند.
- اگر علاقمند بودید که چاپگر خاصی را معرفی کنید (برای مثال یک چاپگر تعریف شده در شبکه)، می‌توانید خاصیت PrinterName را مقدار دهی نمائید.
- محل نصب Adobe reader از رجیستری ویندوز استخراج می‌شود. اما اگر محل نصب برنامه استاندارد نبود، نیاز است خاصیت AdobeReaderPath مقدار دهی گردد.
- تحت هر شرایطی برنامه Adobe reader ظاهر خواهد شد؛ حتی اگر در حین آغاز پروسه سعی در مخفی کردن پنجره آن نمائید. اینکار به عمد جهت مسایل امنیتی در این برنامه درنظر گرفته شده است تا کاربر بداند که پروسه چاپ آغاز شده است.
بازخوردهای پروژه‌ها
استفاده از DataAnnotations , dynamic column
سلام 
یه سوال از حضورتون این کدی که در حال حاضر استفاده میکنم ، . میخوام از (DisplayName) استفاده کنم که باز هم از خود نام پروپرتی‌ها در codefirst  استفاده میکنه ، میشه بفرمایید کدوم قسمت رو باید تغییر بدم
public IPdfReportData CreatePdfReport(string title)
        {
            if (title=="" || title==null)
            {
                title = "گذارش";
            }
            return new PdfReport().DocumentPreferences(doc =>
               {
                   doc.RunDirection(PdfRunDirection.RightToLeft);
                   doc.Orientation(PageOrientation.Portrait);
                   doc.PageSize(PdfPageSize.A4);
                   doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
               })
               .DefaultFonts(fonts =>
               {
                   fonts.Path(AppPath.ApplicationPath + "\\fonts\\BNazanin.ttf",
                                     Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
               })
                .PagesFooter(footer =>
                {
                    footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
                })
                .PagesHeader(header =>
                {
                    header.DefaultHeader(defaultHeader =>
                    {
                       // defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                        defaultHeader.Message(title);
                    });
                })
                .MainTableTemplate(template =>
                {
                    template.BasicTemplate(BasicTemplate.SilverTemplate);
                })
                .MainTablePreferences(table =>
                {
                    table.ColumnsWidthsType(TableColumnWidthType.Relative);
                })
                .MainTableDataSource(dataSource =>
                {
                    Cash.Models.ApplicationDbContext db = new Models.ApplicationDbContext();
                    var listOfRows = db.Users.Join(

                        db.CheckOuts, u => u.Id, c => c.ApplicationUserID,
                        (u, c) => new { u.FName, u.Lname, u.NationalCode, c.Creditor, c.Debtor, c.Decsription }
                        ).Where(x => x.Creditor != 0).ToList();


                    dataSource.AnonymousTypeList(listOfRows);
                })
                .MainTableSummarySettings(summary =>
                {
                    summary.OverallSummarySettings("جمع کل");
                    summary.PreviousPageSummarySettings("نقل از صفحه قبل");
                    summary.PageSummarySettings("جمع صفحه");
                })
                .MainTableAdHocColumnsConventions(adHocColumns =>
                {
                    //We want sum of the int columns
                    adHocColumns.AddTypeAggregateFunction(
                        typeof(Int64),
                        new AggregateProvider(AggregateFunction.Sum)
                        {
                            DisplayFormatFormula = obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)
                        });
                    adHocColumns.AddTypeAggregateFunction(
                      typeof(Decimal),
                      new AggregateProvider(AggregateFunction.Sum)
                      {
                          DisplayFormatFormula = obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)
                      });

                    //We want to dispaly all of the dateTimes as ShamsiDateTime
                    adHocColumns.AddTypeDisplayFormatFormula(
                        typeof(DateTime),
                        data => { return PersianDate.ToPersianDateTime((DateTime)data); }
                    );
                    adHocColumns.ShowRowNumberColumn(true);
                    adHocColumns.RowNumberColumnCaption("ردیف");
                })
                .MainTableEvents(events =>
                {
                    events.DataSourceIsEmpty(message: "هیچ اطلاعاتی موجود نیست");
                })
                .Export(export =>
                {
                    export.ToExcel();
                    export.ToXml();
                })
                .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\AdHocColumnsSampleRpt.pdf"));
        }

مطالب
نحوه ایجاد فیلدهای محاسباتی در PdfReport
در گزارشات، گاهی از اوقات نیاز خواهد شد تا تعدادی ستون جدید را بر اساس مقادیر فیلدهای موجود در منبع داده گزارش، به صورت پویا محاسبه و تولید کنیم (ایجاد ستون‌هایی که در منبع داده وجود خارجی ندارند). در ادامه با نحوه انجام اینکار در PdfReport آشنا خواهیم شد.
در ابتدا کدهای کامل گزارش این قسمت را در ادامه ملاحظه خواهید کرد (در این کلاس از دو کلاس User و AppPath قسمت قبل نیز استفاده شده است):
using System;
using System.Collections.Generic;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.CalculatedFields
{
    public class CalculatedFieldsPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                            Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.RainyDayTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.Relative);
             })
             .MainTableDataSource(dataSource =>
             {
                 var listOfRows = new List<User>();
                 for (int i = 0; i < 220; i++)
                 {
                     listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                 }
                 dataSource.StronglyTypedList<User>(listOfRows);
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "رکوردی یافت نشد.");
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
                 summary.PageSummarySettings("جمع صفحه");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.Width(1);
                     column.HeaderCell("ردیف", captionRotation: 90);
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Id);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.Width(2);
                     column.HeaderCell("شماره");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Name);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(2);
                     column.Width(2);
                     column.HeaderCell("نام");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.LastName);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     column.Width(3);
                     column.HeaderCell("نام خانوادگی");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("CF1");
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var name = list.GetSafeStringValueOf<User>(x => x.Name);
                             var lastName = list.GetSafeStringValueOf<User>(x => x.LastName);
                             return name + " - " + lastName;
                         });
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Balance);
                     column.HeaderCell("موجودی");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.Width(2);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(5);
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("CF2");
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var balance = list.GetValueOf<User>(x => x.Balance);
                             return (long)balance * 3;
                         });
                     column.IsVisible(true);
                     column.Order(6);
                 });
             })
             .Export(export =>
             {
                 export.ToExcel(footer: "Footer text", header: "&24&U&\"Arial,Regular Bold\" New rpt.", pageLayoutView: true);
                 export.ToXml();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptCalculatedFieldsSample.pdf"));
        }
    }
}

توضیحات:
- در این مثال از یک قلم سفارشی به نام Iranian Sans استفاده شده که در پوشه bin\fonts سورس به روز شده پروژه، قابل دریافت است.
- برخلاف قسمت قبل، از متد table.NumberOfDataRowsPerPage استفاده نشده است. به این ترتیب تعداد ردیف‌ها به صورت خودکار بر اساس اندازه صفحه محاسبه می‌شود.
- منبع داده تعریف شده توسط  dataSource.StronglyTypedList، بسیار مناسب است جهت کار با انواع و اقسام ORMها. تفاوتی نمی‌کند از چه ORM ایی استفاده می‌کنید؛ همینقدر که خروجی کار شما یک List باشد، در اینجا قابل استفاده خواهد بود. حتی نیازی هم به بانک اطلاعاتی نیست و در این مثال از یک منبع داده درون حافظه‌ای استفاده شده است.
- توضیحاتی در مورد ColumnsWidthsType :
برای تعیین عرض ستون‌ها، چهار حالت بر اساس مقادیر enum ایی به نام TableColumnWidthType میسر است:
الف) Relative : عرض نسبی. به این معنا که اگر سه ستون با عرض‌های 2, 1, 1 تعریف کنید، کل عرض صفحه به 4 قسمت تقسیم می‌شود. از این 4 قسمت، 2 قسمت به ستون اول و یک قسمت به ستون دوم و همچنین یک قسمت به ستون سوم اختصاص خواهد یافت.
ب) Absolute : در این حالت باید عرض ستون‌ها را دقیقا بر اساس user space units مشخص کنید.
ج) FitToContent : سعی خواهد کرد بر اساس طول محتوای یک سلول، عرض بهینه‌ای را محاسبه کند. در این حالت نیازی به قید column.Width نیست.
د) EquallySized : به صورت خودکار عرض تمام ستون‌ها را یکسان محاسبه می‌کند. در این حالت نیازی به قید column.Width نیست.
- اولین فیلد محاسباتی که در PdfReport به صورت توکار و خودکار در اختیار شما است، فیلد شماره ردیف می‌باشد که به صورت زیر مشخص می‌شود:
 column.IsRowNumber(true);
بنابراین نیازی نیست تا منبع داده شما شامل یک ستون اضافی به نام ردیف باشد. PdfReport این مورد را به صورت خودکار تولید خواهد کرد.
- سپس دو فیلد و ستون محاسباتی در گزارش فوق قابل مشاهده هستند:
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("CF1");
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var name = list.GetSafeStringValueOf<User>(x => x.Name);
                             var lastName = list.GetSafeStringValueOf<User>(x => x.LastName);
                             return name + " - " + lastName;
                         });
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                 });
برای اینکه یک فیلد پویا را به مجموعه فیلدهای مهیا شده توسط منبع داده اضافه کنیم، از متد CalculatedField با پارامتر اول مساوی true استفاده خواهیم کرد. سپس نیاز است نحوه محاسبه این فیلد را مشخص کنیم. امضای متد CalculatedField به صورت زیر است:
 public void CalculatedField(bool isCalculatedField, Func<IList<CellData>, object> calculatedFieldFormula)
به این معنا که توسط آرگومان دوم آن، لیست کلیه مقادیر ردیف جاری در اختیار شما قرار خواهد گرفت. در این بین فرصت خواهیم داشت بر این اساس، فیلد و مقدار جدیدی را تولید کرده و بازگشت دهیم؛ که نمونه‌ای از اینکار را در فیلد محاسبانی CF1 فوق مشاهده می‌کنید.
باید دقت داشت که نام خواص (column.PropertyName) باید منحصربفرد باشند و گرنه برنامه با یک استثناء متوقف خواهد شد. اگر ستون معرفی شده متناظر است با یک فیلد یا خاصیت منبع داده، باید PropertyName با رعایت کوچکی و بزرگی حروف، معادل فیلد متناظر باشد. اگر ستون تعریف شده یک فیلد محاسباتی است، تنها کافی است یک نام دلخواه غیرتکراری را ذکر کرد.
همچنین جهت سهولت کار، در فضای نام PdfRpt.Core.Helper، تعدادی متد برای کار با لیستی از CellDataها تدارک دیده شده‌اند؛ که نمونه‌ای از آن‌را در اینجا با استفاده از متدهای GetSafeStringValueOf ملاحظه می‌کنید.

- فیلد محاسباتی دیگری نیز در این گزارش قابل مشاهده است:
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("CF2");
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var balance = list.GetValueOf<User>(x => x.Balance);
                             return (long)balance * 3;
                         });
                     column.IsVisible(true);
                     column.Order(6);
                 });
در اینجا در متد  column.CalculatedField، مقدار فیلد موجودی (Balance) ردیف جاری دریافت شده و سپس مقدار دلخواه جدیدی تولید و بازگشت داده شده است.
همچنین توسط متد column.AggregateFunction، مشخص کرده‌ایم که این ستون جدید نیاز به جمع پایین صفحه هم دارد. به علاوه توسط متد column.ColumnItemsTemplate، با مشخص سازی DisplayFormatFormula، پیش از نمایش اطلاعات فیلد جاری، مقدار آن‌را دریافت و فرمت کرده‌ایم؛ که در اینجا سه رقم جداکننده اعداد اضافه شده است.
ذکر متد template.TextBlock اختیاری است و حالت پیش فرض می‌باشد. قالب‌های دیگری نیز در اینجا تعریف شده‌اند که در مثال‌های قسمت‌های بعدی آن‌ها را بررسی خواهیم کرد (امکان نمایش تصویر، لینک، بارکد و غیره).

 
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت چهارم - data binding
با سلام
برای نمایش واحد پول بصورت ریال در آنگولار، می‌توانید از پایپ زیر استفاده کنید.
import { Pipe } from '@angular/core';
@Pipe({
    name: 'persianCurrency'
})
export class PersianCurrency {
    transform(value: number): string {
        return Intl.NumberFormat('fa-FA', { style: 'currency', currency: 'IRR' }).format(value);
    }
}
بعداز ذخیره ، در فایل app.module  ابتدا فایل را از مسیر ذخیره شده ایمپورت کرده و سپس در قسمت declarations ثبت نمایید.
برای استفاده‌های بعدی کافیست تا از بعداز درج پایپ عبارت persianCurrency را وارد کنید.
مثال:
{{myMoney | persianCurrency}}
 
نظرات مطالب
تعیین شماره نگارش IE مورد استفاده در Web Browser Control
یک نکته‌ی تکمیلی
تعیین شماره نگارش IE مورد استفاده‌ی توسط برنامه به صورت خودکار:
using System;
using System.Diagnostics;
using Microsoft.Win32;
using System.Windows.Forms;

namespace Core
{
    public static class UseLatestVersionOfIE
    {
        /// <summary>
        /// Use the latest version of IE in WebBrowser control
        /// </summary>
        public static void SetWebBrowserVersion()
        {
            RegistryKey regkey = null;
            try
            {
                regkey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION", writable: true);
                if (regkey == null)
                {
                    return;
                }

                var regVal = getInstalledIEVersion();
                var appName = string.Format("{0}.exe", Process.GetCurrentProcess().ProcessName);
                regkey.SetValue(appName, regVal, RegistryValueKind.DWord);
            }
            catch (Exception ex)
            {
              // todo: log ...
            }
            finally
            {
                if (regkey != null)
                {
                    regkey.Close();
                }
            }
        }

        private static int getInstalledIEVersion()
        {
            int browserVer;
            using (var wb = new WebBrowser())
            {
                browserVer = wb.Version.Major;
            }

            int regVal;
            if (browserVer >= 11)
                regVal = 11001;
            else
                switch (browserVer)
                {
                    case 10:
                        regVal = 10001;
                        break;
                    case 9:
                        regVal = 9999;
                        break;
                    case 8:
                        regVal = 8888;
                        break;
                    default:
                        regVal = 7000;
                        break;
                }
            return regVal;
        }
    }
}
در اینجا شماره نگارش IE از کنترل WebBrowser دریافت می‌شود و همیشه به آخرین نگارش تنظیم خواهد شد و همچنین چون از Registry.CurrentUser استفاده می‌کند، نیازی به دسترسی مدیریتی برای اعمال ندارد.