مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
ASP.NET Core 2.0 به همراه یک AuthenticationMiddleware است که یکی از قابلیت‌های این میان‌افزار، افزودن اطلاعات کاربر و یا همان HttpContext.User به یک کوکی رمزنگاری شده و سپس اعتبارسنجی این کوکی در درخواست‌های بعدی کاربر است. این مورد سبب خواهد شد تا بتوان بدون نیاز به پیاده سازی سیستم کامل ASP.NET Core Identity، یک سیستم اعتبارسنجی سبک، ساده و سفارشی را تدارک دید.



تعریف موجودیت‌های مورد نیاز جهت طراحی یک سیستم اعتبارسنجی

در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت می‌توان طراحی تمام قسمت‌ها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی ساده‌ی ما، شامل جدول کاربران و نقش‌های آن‌ها خواهد بود و این دو با هم رابطه‌ی many-to-many دارند. به همین جهت جدول UserRole نیز در اینجا پیش بینی شده‌است.

جدول کاربران

    public class User
    {
        public User()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }

        public string Username { get; set; }

        public string Password { get; set; }

        public string DisplayName { get; set; }

        public bool IsActive { get; set; }

        public DateTimeOffset? LastLoggedIn { get; set; }

        /// <summary>
        /// every time the user changes his Password,
        /// or an admin changes his Roles or stat/IsActive,
        /// create a new `SerialNumber` GUID and store it in the DB.
        /// </summary>
        public string SerialNumber { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
در اینجا SerialNumber فیلدی است که با هر بار ویرایش اطلاعات کاربران باید از طرف برنامه به روز رسانی شود. از آن جهت غیرمعتبر سازی کوکی کاربر استفاده خواهیم کرد. برای مثال اگر خاصیت فعال بودن او تغییر کرد و یا نقش‌های او را تغییر دادیم، کاربر در همان لحظه باید logout شود. به همین جهت چنین فیلدی در اینجا در نظر گرفته شده‌است تا با بررسی آن بتوان وضعیت معتبر بودن کوکی او را تشخیص داد.

جدول نقش‌های کاربران

    public class Role
    {
        public Role()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
البته این سیستم ساده دارای یک سری نقش ثابت و مشخص است.
    public static class CustomRoles
    {
        public const string Admin = nameof(Admin);
        public const string User = nameof(User);
    }
که این نقش‌ها را در ابتدای کار برنامه به بانک اطلاعات اضافه خواهیم کرد.


جدول ارتباط نقش‌ها با کاربران و برعکس

    public class UserRole
    {
        public int UserId { get; set; }
        public int RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }
وجود این جدول در EF Core جهت تعریف یک رابطه‌ی many-to-many ضروری است.


تعریف Context برنامه و فعالسازی Migrations در EF Core 2.0

DbContext برنامه را به صورت ذیل در یک اسمبلی دیگر اضافه خواهیم کرد:
    public interface IUnitOfWork : IDisposable
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;

        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    }

    public class ApplicationDbContext : DbContext, IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<User> Users { set; get; }
        public virtual DbSet<Role> Roles { set; get; }
        public virtual DbSet<UserRole> UserRoles { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<User>(entity =>
            {
                entity.Property(e => e.Username).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Username).IsUnique();
                entity.Property(e => e.Password).IsRequired();
                entity.Property(e => e.SerialNumber).HasMaxLength(450);
            });

            builder.Entity<Role>(entity =>
            {
                entity.Property(e => e.Name).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Name).IsUnique();
            });

            builder.Entity<UserRole>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.RoleId });
                entity.HasIndex(e => e.UserId);
                entity.HasIndex(e => e.RoleId);
                entity.Property(e => e.UserId);
                entity.Property(e => e.RoleId);
                entity.HasOne(d => d.Role).WithMany(p => p.UserRoles).HasForeignKey(d => d.RoleId);
                entity.HasOne(d => d.User).WithMany(p => p.UserRoles).HasForeignKey(d => d.UserId);
            });
        }
    }
در اینجا موجودیت‌های برنامه به صورت DbSet در معرض دید EF Core 2.0 قرار گرفته‌اند. همچنین رابطه‌ی Many-to-Many بین نقش‌ها و کاربران نیز تنظیم شده‌است.
سازنده‌ی کلاس به همراه پارامتر DbContextOptions است تا بتوان آن‌را در آغاز برنامه تغییر داد.


فعالسازی مهاجرت‌ها در EF Core 2.0

EF Core 2.0 برخلاف نگارش‌های قبلی آن به دنبال کلاسی مشتق شده‌ی از IDesignTimeDbContextFactory می‌گردد تا بتواند نحوه‌ی وهله سازی ApplicationDbContext را دریافت کند. در اینجا چون DbContext تعریف شده دارای یک سازنده‌ی با پارامتر است، EF Core 2.0 نمی‌داند که چگونه باید آن‌را در حین ساخت مهاجرت‌ها و اعمال آن‌ها، وهله سازی کند. کار کلاس ApplicationDbContextFactory ذیل دقیقا مشخص سازی همین مساله است:
    /// <summary>
    /// Only used by EF Tooling
    /// </summary>
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var basePath = Directory.GetCurrentDirectory();
            Console.WriteLine($"Using `{basePath}` as the BasePath");
            var configuration = new ConfigurationBuilder()
                                    .SetBasePath(basePath)
                                    .AddJsonFile("appsettings.json")
                                    .Build();
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            builder.UseSqlServer(connectionString);
            return new ApplicationDbContext(builder.Options);
        }
    }
کاری که در اینجا انجام شده، خواندن DefaultConnection از قسمت ConnectionStrings فایل appsettings.json است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;"
  },
  "LoginCookieExpirationDays": 30
}
و سپس استفاده‌ی از آن جهت تنظیم رشته‌ی اتصالی متد UseSqlServer و در آخر وهله سازی ApplicationDbContext.
کار یافتن این کلاس در حین تدارک و اعمال مهاجرت‌ها توسط EF Core 2.0 خودکار بوده و باید محل قرارگیری آن دقیقا در اسمبلی باشد که DbContext برنامه در آن تعریف شده‌است.


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

پس از مشخص شدن ساختار موجودیت‌ها و همچنین Context برنامه، اکنون می‌توان لایه سرویس برنامه را به صورت ذیل تکمیل کرد:

سرویس کاربران
    public interface IUsersService
    {
        Task<string> GetSerialNumberAsync(int userId);
        Task<User> FindUserAsync(string username, string password);
        Task<User> FindUserAsync(int userId);
        Task UpdateUserLastActivityDateAsync(int userId);
    }
کار این سرویس ابتدایی کاربران، یافتن یک یک کاربر بر اساس Id او و یا کلمه‌ی عبور و نام کاربری او است. از این امکانات در حین لاگین و یا اعتبارسنجی کوکی کاربر استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس نقش‌های کاربران
    public interface IRolesService
    {
        Task<List<Role>> FindUserRolesAsync(int userId);
        Task<bool> IsUserInRole(int userId, string roleName);
        Task<List<User>> FindUsersInRoleAsync(string roleName);
    }
از این سرویس برای یافتن نقش‌های کاربر لاگین شده‌ی به سیستم و افزودن آن‌ها به کوکی رمزنگاری شده‌ی اعتبارسنجی او استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس آغاز بانک اطلاعاتی

    public interface IDbInitializerService
    {
        void Initialize();
        void SeedData();
    }
از این سرویس در آغاز کار برنامه برای اعمال خودکار مهاجرت‌های تولیدی و همچنین ثبت نقش‌های آغازین سیستم به همراه افزودن کاربر Admin استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس اعتبارسنجی کوکی‌های کاربران

یکی از قابلیت‌های میان‌افزار اعتبارسنجی ASP.NET Core 2.0، رخ‌دادی است که در آن اطلاعات کوکی دریافتی از کاربر، رمزگشایی شده و در اختیار برنامه جهت تعیین اعتبار قرار می‌گیرد:
    public interface ICookieValidatorService
    {
        Task ValidateAsync(CookieValidatePrincipalContext context);
    }

    public class CookieValidatorService : ICookieValidatorService
    {
        private readonly IUsersService _usersService;
        public CookieValidatorService(IUsersService usersService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));
        }

        public async Task ValidateAsync(CookieValidatePrincipalContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                await handleUnauthorizedRequest(context);
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }

        private Task handleUnauthorizedRequest(CookieValidatePrincipalContext context)
        {
            context.RejectPrincipal();
            return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
کار این سرویس، تعیین اعتبار موارد ذیل است:
- آیا کوکی دریافت شده دارای اطلاعات HttpContext.User است؟
- آیا این کوکی به همراه اطلاعات فیلد SerialNumber است؟
- آیا این کوکی به همراه Id کاربر است؟
- آیا کاربری که بر اساس این Id یافت می‌شود غیرفعال شده‌است؟
- آیا کاربری که بر اساس این Id یافت می‌شود دارای SerialNumber یکسانی با نمونه‌ی موجود در بانک اطلاعاتی است؟

اگر خیر، این اعتبارسنجی رد شده و بلافاصله کوکی کاربر نیز معدوم خواهد شد.


تنظیمات ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0

تنظیمات کامل ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0 را در فایل Startup.cs می‌توانید مشاهده کنید.

ابتدا سرویس‌های برنامه معرفی شده‌اند:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IUsersService, UsersService>();
            services.AddScoped<IRolesService, RolesService>();
            services.AddScoped<ISecurityService, SecurityService>();
            services.AddScoped<ICookieValidatorService, CookieValidatorService>();
            services.AddScoped<IDbInitializerService, DbInitializerService>();

سپس تنظیمات مرتبط با ترزیق وابستگی‌های ApplicationDbContext برنامه انجام شده‌است. در اینجا رشته‌ی اتصالی، از فایل appsettings.json خوانده شده و سپس در اختیار متد UseSqlServer قرار می‌گیرد:
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                    serverDbContextOptionsBuilder =>
                        {
                            var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                            serverDbContextOptionsBuilder.CommandTimeout(minutes);
                            serverDbContextOptionsBuilder.EnableRetryOnFailure();
                        });
            });

در ادامه تعدادی Policy مبتنی بر نقش‌های ثابت سیستم را تعریف کرده‌ایم. این کار اختیاری است اما روش توصیه شده‌ی در ASP.NET Core، کار با Policyها است تا کار مستقیم با نقش‌ها. Policy‌ها انعطاف پذیری بیشتری را نسبت به نقش‌ها ارائه می‌دهند و در اینجا به سادگی می‌توان چندین نقش و یا حتی Claim را با هم ترکیب کرد و به صورت یک Policy ارائه داد:
            // Only needed for custom roles.
            services.AddAuthorization(options =>
                    {
                        options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
                        options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                    });

قسمت اصلی تنظیمات میان افزار اعتبارسنجی مبتنی بر کوکی‌ها در اینجا قید شده‌است:
            // Needed for cookie auth.
            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(options =>
                {
                    options.SlidingExpiration = false;
                    options.LoginPath = "/api/account/login";
                    options.LogoutPath = "/api/account/logout";
                    //options.AccessDeniedPath = new PathString("/Home/Forbidden/");
                    options.Cookie.Name = ".my.app1.cookie";
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = context =>
                        {
                            var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
                            return cookieValidatorService.ValidateAsync(context);
                        }
                    };
                });
ابتدا مشخص شده‌است که روش مدنظر ما، اعتبارسنجی مبتنی بر کوکی‌ها است و سپس تنظیمات مرتبط با کوکی رمزنگاری شده‌ی برنامه مشخص شده‌اند. تنها قسمت مهم آن CookieAuthenticationEvents است که نحوه‌ی پیاده سازی آن‌را با معرفی سرویس ICookieValidatorService پیشتر بررسی کردیم. این قسمت جائی است که پس از هر درخواست به سرور اجرا شده و کوکی رمزگشایی شده، در اختیار برنامه جهت اعتبارسنجی قرار می‌گیرد.

کار نهایی تنظیمات میان افزار اعتبارسنجی در متد Configure با فراخوانی UseAuthentication صورت می‌گیرد. اینجا است که میان افزار، به برنامه معرفی خواهد شد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseAuthentication();

همچنین پس از آن، کار اجرای سرویس آغاز بانک اطلاعاتی نیز انجام شده‌است تا نقش‌ها و کاربر Admin را به سیستم اضافه کند:
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var dbInitializer = scope.ServiceProvider.GetService<IDbInitializerService>();
                dbInitializer.Initialize();
                dbInitializer.SeedData();
            }


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

پس از این مقدمات به مرحله‌ی آخر پیاده سازی این سیستم اعتبارسنجی می‌رسیم.

پیاده سازی Login
در اینجا از سرویس کاربران استفاده شده و بر اساس نام کاربری و کلمه‌ی عبور ارسالی به سمت سرور، این کاربر یافت خواهد شد.
در صورت وجود این کاربر، مرحله‌ی نهایی کار، فراخوانی متد الحاقی HttpContext.SignInAsync است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                return Unauthorized();
            }

            var loginCookieExpirationDays = _configuration.GetValue<int>("LoginCookieExpirationDays", defaultValue: 30);
            var cookieClaims = await createCookieClaimsAsync(user).ConfigureAwait(false);
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                cookieClaims,
                new AuthenticationProperties
                {
                    IsPersistent = true, // "Remember Me"
                    IssuedUtc = DateTimeOffset.UtcNow,
                    ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
                });

            await _usersService.UpdateUserLastActivityDateAsync(user.Id).ConfigureAwait(false);

            return Ok();
        }
مهم‌ترین کار متد HttpContext.SignInAsync علاوه بر تنظیم طول عمر کوکی کاربر، قسمت createCookieClaimsAsync است که به صورت ذیل پیاده سازی شده‌است:
        private async Task<ClaimsPrincipal> createCookieClaimsAsync(User user)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
            identity.AddClaim(new Claim("DisplayName", user.DisplayName));

            // to invalidate the cookie
            identity.AddClaim(new Claim(ClaimTypes.SerialNumber, user.SerialNumber));

            // custom data
            identity.AddClaim(new Claim(ClaimTypes.UserData, user.Id.ToString()));

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
            }

            return new ClaimsPrincipal(identity);
        }
در اینجا است که اطلاعات اضافی کاربر مانند Id او یا نقش‌های او به کوکی رمزنگاری شده‌ی تولیدی توسط HttpContext.SignInAsync اضافه شده و در دفعات بعدی و درخواست‌های بعدی او، به صورت خودکار توسط مرورگر به سمت سرور ارسال خواهند شد. این کوکی است که امکان کار با MyProtectedApiController و یا MyProtectedAdminApiController را فراهم می‌کند. اگر شخص لاگین کرده باشد، بلافاصله قابلیت دسترسی به امکانات محدود شده‌ی توسط فیلتر Authorize را خواهد یافت. همچنین در این مثال چون کاربر Admin لاگین می‌شود، امکان دسترسی به Policy مرتبطی را نیز خواهد یافت:
[Route("api/[controller]")]
[Authorize(Policy = CustomRoles.Admin)]
public class MyProtectedAdminApiController : Controller

پیاده سازی Logout

متد الحاقی HttpContext.SignOutAsync کار Logout کاربر را تکمیل می‌کند.
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return true;
        }


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

در فایل index.html ، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
ایجاد لیستی از کلاسی جنریک
کلاس جنریک زیر را در نظر بگیرید:
public class Column<T>

{
public string Name { set; get; }
public T Data { set; get; }
}

مشکلی که با این نوع کلاس‌ها وجود دارد این است که نمی‌توان مثلا لیست زیر را در مورد آن‌ها تعریف کرد:

IList<Column<T>> myList = new List<Column<T>>();



به عبارتی می‌خواهیم یک لیست از کلاسی جنریک داشته باشیم. راه حل انجام آن به صورت زیر است:

using System.Collections;


namespace Tests
{
public interface IColumn
{
string Name { set; get; }
object Data { set; get; }
}

public class Column<T> : IColumn
{
public string Name { set; get; }

public T Data { set; get; }

object IColumn.Data
{
get { return this.Data; }
set { this.Data = (T)value; }
}
}
}

ابتدا یک اینترفیس عمومی را همانند اعضای کلاس Column تعریف می‌کنیم که در آن بجای T از object‌ استفاده شده است. سپس یک پیاده سازی جنریک از این اینترفیس را ارائه خواهیم داد؛ با این تفاوت که اینبار خاصیت Data مربوط به اینترفیس، به صورت خصوصی و صریح با استفاده از IColumn.Data تعریف می‌شود و نمونه‌ی جنریک هم نام آن، عمومی خواهد بود.
اکنون می‌توان نوشت:

var myList = new List<IColumn>();


برای مثال در این حالت تعریف لیست زیر که از تعدادی وهله‌ی کلاسی جنریک ایجاد شده، کاملا مجاز می‌باشد:

var myList = new List<IColumn>

{
new Column<int> { Data = 1, Name = "Col1"},
new Column<double> { Data = 1.2, Name = "Col2"}
};

خوب، تا اینجا یک مرحله پیشرفت است.اکنون اگر بخواهیم در این لیست، Data مثلا عنصری را که نامش Col1 است، دریافت کنیم چه باید کرد؟ آن هم نه به شکل object بلکه از نوع T مشخص:

static T GetColumnData<T>(IList<IColumn> list, string name)

{
var column = (Column<T>)Convert.ChangeType(list.Single(s => s.Name.Equals(name)), typeof(Column<T>), null);
return column.Data;
}

و نمونه‌ای از استفاده آن:

int data = GetColumnData<int>(myList, "Col1");


مطالب
فرمت شرطی اطلاعات به کمک تریگرها در WPF
فرض کنید در یک لیست، تعداد زیادی صفر وجود دارند و تنها معدودی از آن‌ها دارای مقداری متفاوت هستند. شاید بد نباشد برای کاهش نویز صفحه، صفرها نمایش داده نشوند و در کل لیست، فقط مقادیر بیشتر از صفر مشخص باشند. برای اینکار راه حل‌های زیادی وجود دارند؛ منجمله، استفاده از تبدیلگرها. اما با استفاده از تریگرهای WPF اینکار را با چند سطر کد ساده، در همان فایل XAML یا یک شیوه‌نامه جدید می‌توان انجام داد.

تعریف تریگر مخفی سازی یک برچسب

        <Style TargetType="TextBlock" x:Key="TextBlockStyle1">
            <Style.Triggers>
                <Trigger Property="Text" Value="0">
                    <Setter Property="Visibility" Value="Collapsed" />
                </Trigger>
            </Style.Triggers>
        </Style>
برای تعریف تریگر، ابتدا در یک Style جدید، مشخص می‌کنیم که اطلاعات تعریف شده باید به چه نوع المانی اعمال شوند. سپس در قسمت Style.Triggers تعیین می‌کنیم که اگر خاصیت Text این المان مساوی صفر بود، مقدار خاصیت Visibility آن به Collapsed تغییر یابد تا مخفی شود.

این تعاریف در مورد یک TextBlock بود. برای کنترل Label به علت نداشتن خاصیت Text و داشتن خاصیت Content می‌توان به نحو ذیل عمل کرد:
        <Style TargetType="Label" x:Key="LabelStyle1">
            <Style.Triggers>
                <Trigger Property="Content">
                    <Trigger.Value>
                        <system:Int32>0</system:Int32>
                    </Trigger.Value>
                    <Setter Property="Visibility" Value="Collapsed" />
                </Trigger>
            </Style.Triggers>
        </Style>
چون خاصیت Content می‌تواند یک object نیز باشد، توسط Trigger.Value مقدار آن به یک Int32 تبدیل شده و سپس بر این مبنا تصمیم گیری می‌شود.

برای اعمال آن‌ها نیز می‌توان به نحو ذیل عمل کرد:
 <TextBlock Text="{Binding Rating, Mode=OneTime}"
Style="{StaticResource TextBlockStyle1}" />


با اعمال این تریگر، مقادیر صفر در ستون rating نمایش داده نخواهند شد.

یک مثال کامل را در این زمینه از اینجا می‌توانید دریافت کنید
WpfVisibilityTriggers.zip


برای مطالعه بیشتر
Trigger, DataTrigger & EventTrigger
WPF MultiTrigger and MultiDataTrigger

مطالب
تفاوت انواع var و dynamic

در ابتدا به توضیحاتی درباره کنترل نوع به صورت ایستا و کنترل نوع در زمان اجرا، توجه کنید:

کنترل نوع ایستا (Static Type Checkingکامپایلر را قادر به بررسی درستی برنامه می‎کند، بدون آنکه آن را اجرا کند.  مثلاً کد زیر با خطا مواجه می‎‎شود:
int x = "5";
کنترل نوع در زمان اجرا (RunTime Type Checking)، هنگامی که برنامه اجرا می‎شود این کنترل توسط CLR صورت می‌گیرد و موقع تایپ کد، خطایی گرفته نمی‎شود مثلاً:

object y = "5";
int z = (int) y; //downcast خطای زمان اجرا و شکست 
downcast به عمل تبدیل نوع کلاس پایه به یکی از کلاس‎های مشتق شده، گفته می‌شود.
 
پس از ذکر مقدمه بالا به این سؤال می‎پردازیم که تفاوت انواع var و dynamic  چیست؟
کلمه کلیدی var و کلمه کلیدی dynamic  علی‌الظاهر کاربرد یکسانی دارند اما تفاوت اساسی آن‎ها عبارت است از این‌که  نوع واقعی متغیرهایی از نوعvar ، توسط کامپایلر تعیین می‎شود. یعنی متغیرهایی از نوع var کنترل نوعشان به صورت ایستاست اما نوع واقعی‌ متغیرهایی از نوعdynamic ، در زمان اجرا مشخص می‎شود. یعنی متغیرهایی از نوع  dynamic کنترل نوعشان به صورت کنترل نوع در زمان اجراست. به کد زیر توجه کنید:
dynamic x = "hello"; // این متغیر در زمان کامپایل از نوع دینامیک است و در زمان اجرا از نوع رشته‌ای است 
var y = "hello"; //این متغیر هم در زمان کامپایل و هم در زمان اجرا از نوع رشته‌ای است 
int i = x; // خطای زمان اجرا
int j = y; //خطای زمان کامپایل

همچنین متغیری که نوعشvar  است می‎تواند مقداری از نوع dynamic  را شامل شود.

dynamic x = "hello";
var y = x; // این متغیر در زمان کامپایل دینامیک است
int z = y; // خطای زمان اجرا
 
مطالب
طراحی گزارش در Stimulsoft Reports.Net – بخش 2
در این بخش هم به معرفی ابزار و امکانات این گزارش‌ساز خواهم پرداخت، که شامل بند Group , فیلد محاسباتی و کامپوننتهای Panel,Clone و همچنین نحوه ایجاد یک گزارش به صورت Master-Detail خواهد بود.
ابتدا برای شروع به شیوه‌ای که در بخش 1 بیان شد یک دیکشنری ایجاد کنید. بر روی صفحه طراحی رایت کلیک کنید و گزینه Design را انتخاب نمایید. فرم Page Setup ظاهر می‌شود، در پنل سمت چپ این فرم گزینه Columns را انتخاب نمایید. سپس مقادیر 2 و 0.1 را به ترتیب برای "Number of Columns" و "Column Gaps" ست نمایید. سپس بندهای Header , Group و Data را به ترتیب بر روی صفحه طراحی قرار دهید. بر روی بند Group دابل کلیک کنید و همانند تصویر زیر فیلد EmployeeID را از جدول Orders انتخاب نمایید.

حال بر روی بند Data دابل کلیک کنید و در بخش Data Source جدول Orders را انتخاب نمایید. از پنل ابزار یک کامپوننت Text بر روی بند Group قرار دهید و بر روی آن دابل کلیک کنید و در بخش Expression عبارت زیر را وارد نمایید.
Employee ID: {Orders.EmployeeID}  --- GLine: {GroupLine}
*GroupLine یک متغیر سیستمی است که شماره سطر جاری گروه را برمی‌گرداند.
از پنل Dictionary همانند تصویر زیر فیلدهای ShipName و ShipAddress را از جدول Orders بر روی بندهای Header و Data قرار دهید.

حال با مشاهده خروجی گزارش از سربرگ Preview شما شاهد یک صفحه دو ستونه خواهید بود که بر اساس فیلد EployeeID گروه بندی شده است. 
فیلد محاسباتی:
این نوع فیلد زمانی استفاده می‌شود که شما بخواهید در هر سطر از اطلاعات با توجه به مقادیر رکورد جاری محاسباتی را انجام داده و در گزارش نمایش دهید. برای اضافه کردن فیلد محاسباتی در پنل Dictionary بر روی جدول Order Details رایت کلیک کنید و گزینه New Calculated Column را انتخاب نمایید، همانند تصویر زیر

در فرم ظاهر شده مقادیر را به صورت زیر وارد نمایید:

Name: TotalPrice 
Alias: TotalPrice 
Type: decimal
Value: Order_Details.UnitPrice * Order_Details.Quantity
حال یک فیلد به نام TotalPrice به فیلدهای جدول Order Details اضافه شده است.

ایجاد گزارش به صورت Master-Detail
:
برای ایجاد چنین گزارشی نیاز به ارتباط بین جدولها می‌باشد. با توجه به نحوه ایجاد Connection برای این مثال، روابط بین جدولها انتقال داده نشده است ولی شما میتوانید رابطه بین جدولها را اضافه نمایید حتی اگر این رابطه در منبع اطلاعات وجود نداشته باشد. برای این مثال نیاز به دو رابطه بین جدول Orders Detail و جدولهای Orders, Products می‌باشد. برای انجام این کار کافیست در پنل Dictionary بر روی جدول Orders Detail رایت کلیک کنید و گزینه New Relation را انتخاب نماید. همانند تصاویر زیر مقادیر را ست نمایید.

حال بر روی صفحه طراحی بعد از بند DataBand1 به ترتیب بندهای Header و Data و Footer را اضافه نمایید. در بند HeaderBand2 چهار کامپوننت Text قرار دهید و به ترتیب از سمت چپ مقادیر زیر را در خصوصیت Text کامپوننتها قرار دهید.

ProductName
UnitPrice 
Quantity
TotalPrice
سپس بر روی بند DataBand2 دابل کلیک کنید در فرم Data Setup ابتدا در بخش Data Source جدول Orders Detail را انتخاب نمایید؛ در بخش Relation رابطه Orders را انتخاب نمایید و در نهایت در بخش Master Component بند DataBand1 را انتخاب نمایید. حال در بند DataBand2 چهار کامپوننت Text قرار دهید و به ترتیب از سمت چپ مقادیر زیر را در خصوصیت Text کامپوننتها قرار دهید.
{Order_Details.Products.ProductName}
{Order_Details.UnitPrice}
{Order_Details.Quantity}
{Order_Details.TotalPrice}
در بند FooterBand1 یک کامپوننت Text در زیر ستون TotalPrice قرار دهید و مقدار زیر را در خصوصیت Text آن قرار دهید.
{Sum(DataBand2,Order_Details.TotalPrice)}
نتیجه کار طراحی گزارش به شکل زیر خواهد بود.

حال میتوانید خروجی گزارش Master-Detail را از سربرگ Preview مشاهده نمایید. در صورتی که همانند تصویر بالا گزارش را طراحی کرده باشید در خروجی گزارش فاصله‌ای بین سطرها ایجاد شده است که علت آن ارتفاع کمتر کامپوننتهای Text نسبت به بندهای خود می‌باشد. برای رفع این مشکل، شما سه راه حل دارید.

الف: یکسان سازی ارتفاع کاپوننتها با بند دربرگیرنده آنها

ب: ست کردن خصوصیت Can Shrink بند دربرگیرنده کامپوننتها به مقدار true

ج: ست کردن خصوصیت Grow to Height کامپوننتهای Text به مقدار true

در این مثال ما از روش دوم استفاده میکنیم و خصوصیت Can Shrink بندهای HeaderBand2 و DataBand2 را به مقدار true ست میکنیم.

کامپوننتهای Panel و Clone :

Panel: به شما امکان می‌دهد تا کامپوننتها و بندها را دربر بگیرد و در واقع گروهی از کامپوننتها را ایجاد خواهد کرد. پنل می‌تواند مستقل بر روی صفحه طراحی قرار گیرد و یا در یک بند.

Clone: به شما امکان کپی کردن یک بخش از گزارش را میدهد که البته آن بخش فقط میتواند از نوع Panel باشد.

این دو کامپوننت یکی از عوامل قدرت این گزارش‌ساز می‌باشد. برای شروع یک Page (صفحه طراحی) دیگر به گزارش اضافه کنید. می‌توانید با رایت کلیک بر روی نوار سربرگهای محیط طراحی گزینه New Page را انتخاب نمایید. بر روی Page2 یک کامپوننت Panel قرار دهید، سپس از پنل Dictionary جدول Countries را دراگ کرده و در Panel1 رها کنید. در فرم Data تمامی فیلدها و بند Header را انتخاب نمایید، سپس یک کامپوننت Clone به صفحه طراحی اضافه کنید بلافاصله فرم Select Container ظاهر می‌شود، Panel1 را انتخاب کنید. حال شما می‌توانید خروجی گزارش را مشاهده کنید، خروجی Page2 بعد از خروجی Page1 ظاهر خواهد شد.

Report2-1.mrt 

مطالب
نحوه‌ی مشاهده‌ی خروجی SQL تولید شده توسط WCF RIA Services

این روزها با وجود ORMs ، کوئری SQL‌ نوشتن شبیه به دورانی شده که با وجود زبان‌های سطح بالا، عده‌ای علاقمند هستند با استفاده از زبان اسمبلی برنامه نویسی کنند! WCF RIA Services به صورت پیش فرض از entity framework استفاده می‌کند (هر چند می‌توان از سایر ORMs هم استفاده کرد)، بنابراین عنوان صحیح‌تر بحث این خواهد بود: چگونه خروجی SQL تولید شده توسط Entity framework را بررسی کنیم؟

الف) استفاده از SQL Server profiler
اولین برنامه‌ای که از سال‌ها قبل، حتی پیش از ظهور ORMs وجود داشته، برنامه‌ی SQL server profiler است، که عموما در مسیر ذیل قابل دستیابی است:
Start Menu->Programs->Microsoft SQL Server 2008->Performance Tools->SQL Server profiler



نکته مهم:
حین کار با SQL Server profiler ، ممکن است انبوهی از کوئری‌های دیگر مثلا مرتبط با SQL Server agent یا reporting services و غیره نیز لاگ شوند. اما الان ما تنها به کوئری‌های برنامه‌ی خود نیاز داریم. برای این منظور به کانکشن استرینگ خود، گزینه‌ی Application Name=My Application Name را نیز اضافه کنید:

<connectionStrings>
<add name="dmEntities" connectionString="metadata=res://*/Models.dmDataModel.csdl|res://*/Models.dmDataModel.ssdl|res://*/Models.dmDataModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=(local);Initial Catalog=dm;Integrated Security=True;Application Name=My Application Name;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>

اکنون اگر برنامه را با پروفایلر مورد بررسی قرار دهید خروجی به صورت زیر خواهد بود:



برای فیلتر کردن Application Name مورد نظر، در ابتدای کار که یک سشن جدید را آغاز می‌کنید به برگه‌ی events selection مراجعه کرده و بر روی دکمه‌ی column filter کلیک کنید. گزینه‌ی application name را در صفحه‌ی باز شده انتخاب نموده و در قسمت Like آن مطابق تصویر زیر ، نام برنامه‌ی خود را وارد نمائید:




ب) استفاده از IntelliTrace در VS.NET 2010
برنامه را در حالت دیباگ در VS.NET 2010 اجرا کنید. در هر لحظه‌ای می‌توان روی گزینه‌ی Break all کلیک کرد و خروجی SQL تولید شده را نیز علاوه بر اطلاعات دیگر مشاهده نمود:




ج) استفاده از برنامه‌ی حرفه‌ای entity framework profiler
این برنامه از هر دو مورد قبل کاملتر بوده و اساسا برای لاگ کردن کوئری‌ها، مدت زمان اجرا، گزارشگیری از وضعیت برنامه، کدامیک از کوئری‌ها سنگین‌تر هستند، حتی از طریق کدام متد فراخوانی شده‌اند، ارائه‌ی گزارشات و راهنمایی‌هایی در مورد چگونگی بهبود کارآیی برنامه‌ی تهیه شده و امثال آن کاربرد دارد.



استفاده از آن هم بسیار ساده است. ابتدا ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.v4.0 به پروژه‌ی ASP.NET خود اضافه کنید (همان پروژه‌ی هوست مربوط به WCF RIA Service ما). سپس به فایل Global.asax.cs برنامه مراجعه کرده و یک سطر ذیل را اضافه کنید:

protected void Application_Start(object sender, EventArgs e)
{
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
}

از این پس تنها کافی است برنامه‌ی پروفایلر در حال اجرا بوده و برنامه شما نیز اجرا شود. کلیه‌ی تبادلات با دیتابیس لاگ خواهند شد.

نظرات مطالب
معرفی پروژه فروشگاهی Iris Store
موقع کلیک روی تاریخ، قیمت و تخفیف که به صورت x-editable هستند این پیغام خطا در کنسول کروم و فایرفاکس ظاهر می‌شود:


...... 
Uncaught TypeError: Cannot read property 'settings' of undefined

در کامنت‌ها به راه حل اشاره شده است:
Issues with Jquery unobtrusive 

راه حل :
$('#username').on('shown', function() {
    var $innerForm = $(this).data('editable').input.$input.closest('form');
    var $outerForm = $innerForm.parents('form').eq(0);
    $innerForm.data('validator', $outerForm.data('validator'));
});
نظرات مطالب
NHibernate 3.0 و ارائه‌ی جایگزینی جهت ICriteria API
سلام آقای نصیری
من از کد زیر برای جستجو در بانکم بوسیله nhibernate ویرایش 3 استفاده می کنم اما خطا میدهد

Dim Query = ServerRepo.Find(Function(x) x.Country Is Country And x.ServerName Is ServerName)

خطای زیر

Unable to cast object of type 'NHibernate.Hql.Ast.HqlBitwiseAnd' to type 'NHibernate.Hql.Ast.HqlBooleanExpression'.

وقتی بجای Is از = استفاده می کنم خطای زیر را می دهد

Int32 CompareString(System.String, System.String, Boolean)

فکر می کنید مشکل از کجاست با تشکر
مطالب
کامپوننت‌های راهبری سایت در بوت استرپ 4
پس از سیستم طرحبندی بوت استرپ، مهم‌ترین کامپوننت‌های آن، کامپوننت‌های راهبری سایت مانند Navs ،Tabs ،Pills و Navbars هستند. Navbars در بوت استرپ 4 نسبت به نگارش سوم آن بازنویسی کامل شده‌اند و شامل بهبودهای قابل ملاحظه‌ای هستند.


کامپوننت‌های Nav در بوت استرپ 4

کامپوننت‌های گروه Nav‌، در نگارش 4 آن به علت استفاده‌ی از Flexbox، تغییرات بسیاری داشته‌اند و در نتیجه‌ی آن، انعطاف پذیرتر و ساده‌تر شده‌اند.
در ابتدا لیست ساده‌ی زیر را در نظر بگیرید. تنظیمات ابتدایی آن برای تبدیل به منوی راهبری بالای سایت به صورت زیر است:
<body>
    <div class="container">
        <div class="row">
            <section class="col-12">
                <ul class="nav">
                    <li class="nav-item"><a class="nav-link active" href="#">Home</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Mission</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Services</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Staff</a></li>
                    <li class="nav-item"><a class="nav-link disabled" href="#">Testimonials</a></li>
                </ul>
</div>
    </div>
</body>
ابتدا کلاس nav به یک ul اضافه می‌شود. سپس به هر آیتم آن، کلاس nav-item را اضافه می‌کنیم. در آخر به هر لینک آن نیز کلاس nav-link نسبت داده می‌شود:


در اینجا دو کلاس active و disabled نیز به لینک‌های منوی راهبری اضافه شده‌اند. البته این کلاس‌ها تا تکمیل نهایی nav‌، ظاهر آنچنان متفاوتی را ارائه نمی‌دهند.
اولین شیوه‌نامه‌ای را که می‌توان به nav اضافه کرد، nav-pills است:
<ul class="nav nav-pills">


Pills شبیه به دکمه‌ها هستند و در این حالت لینک active، واضح‌تر به نظر می‌رسد.
و یا می‌توان nav-tabs را به nav افزود:
<ul class="nav nav-tabs">


روش دیگر تعریف nav، استفاده از المان nav و سپس حذف ul و li و همچنین nav-item است:
<nav class="nav nav-pills justify-content-center">
    <a class="nav-link active" href="#">Home</a>
    <a class="nav-link" href="#">Mission</a>
    <a class="nav-link" href="#">Services</a>
    <a class="nav-link" href="#">Staff</a>
    <a class="nav-link disabled" href="#">Testimonials</a>
</nav>
در اینجا امکان کار با کلاس‌های Flexbox، مانند justify-content-center را نیز مشاهده می‌کنید. برای مثال برای هدایت منوی راهبری به سمت چپ و یا راست صفحه می‌توان بجای center، از end و یا start استفاده کرد. انجام یک چنین کارهایی در نگارش‌های قبلی بوت استرپ بدون Flexbox، مشکل بودند.
کلاس دیگری را که در اینجا می‌توان استفاده کرد، flex-column است تا آیتم‌های nav، بجای نمایش در یک ردیف، در یک ستون ظاهر شوند:


و یا می‌توان با استفاده از break-points، سبب شد تا اگر اندازه‌ی صفحه بیش از sm بود، آیتم‌های منوی راهبری، ردیفی و اگر کمتر از آن بود (حالت موبایل)، ستونی نمایش داده شوند:
<nav class="nav nav-pills justify-content-center flex-column flex-sm-row">


ایجاد navbars در بوت استرپ 4

Navbar بوت استرپ 4، بازنویسی کامل شده و کار کردن با آن نسبت به نگارش سوم آن بسیار ساده‌تر شده‌است.
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
        </div>
    </nav>

    <div class="container">
با این خروجی:


در اینجا، کار با افزودن کلاس navbar به المان nav شروع می‌شود. سپس هر لینک داخل آن، کلاس‌های nav-item nav-link را پیدا می‌کند. در اینجا اگر آیتمی قرار است به صفحه‌ی جاری اشاره کند، با کلاس active مشخص خواهد شد.
سپس توسط  کلاس‌های bg-dark navbar-dark، رنگ‌های پس زمینه و رنگ متن مشخص شده‌اند. برای مثال می‌توان bg-light navbar-light را نیز آزمایش کرد:
<nav class="navbar bg-light navbar-light navbar-expand-sm">


و یا بجای این رنگ‌های پیش‌فرض، در بوت استرپ 4 می‌توان به سادگی رنگ navbar را توسط یک background-color دلخواه، سفارشی سازی کرد:
<nav class="navbar navbar-dark navbar-expand-sm" style="background-color:red">


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

همچنین اگر دقت کرده باشید از کلاس navbar-expand-sm نیز استفاده شده‌است. حالت پیش‌فرض نمایش آیتم‌های navbar، ستونی است و برای حالت موبایل درنظر گرفته شده‌است. استفاده‌ی از navbar-expand-sm سبب می‌شود تا پس از عرض sm، آیتم‌های navbar همانند شکل‌های فوق، در طی یک ردیف نمایش داده شوند و در عرض کمتر از sm، به صورت یک ستون:


به علاوه آیتم‌های navbar را داخل یک container قرار داده‌ایم:
<div class="container">
      <div class="navbar-nav">
علت اینجا است که چون navbar تعریف شده خارج از container اصلی است، اگر چنین کاری را انجام ندهیم، آیتم‌های آن از سمت چپ صفحه بدون تراز بودن با container ذیل آن نمایش داده خواهند شد. تعریف یک container داخل navbar، این مشکل عدم تراز بودن عمودی را برطرف می‌کند.


تعریف متون و لوگو در navbar بوت استرپ 4

برای تعریف متن لوگوی سایت در navbar به صورت زیر عمل می‌شود:
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <div class="navbar-brand">
                Wisdom Pet Medicine
            </div>
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
        </div>
    </nav>

    <div class="container">
در اینجا با استفاده از کلاس navbar-brand در یک div مجزا، سبب نمایش متن لوگوی سایت شده‌ایم:


و یا می‌توان بجای div، از المان anchor نیز استفاده کرد تا به صورت لینک نمایش داده شود:
<a class="navbar-brand" href="#"> Wisdom Pet Medicine </a>
و بجای متن، تصاویر را نیز می‌توان قرار داد.

برای تعریف متنی در navbar از کلاس navbar-text استفاده می‌شود:
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <a class="navbar-brand d-none d-sm-inline-block" href="#"> Wisdom
                Pet Medicine </a>
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
            <span class="navbar-text d-none d-xl-inline-block">The best in
                traditional and alternate
                medicine</span>
        </div>
    </nav>

    <div class="container">
اما چون این متن طولانی است، بهتر است آن‌را در اندازه‌ی صفحه‌ی xl نمایش دهیم. به همین جهت با افزودن کلاس d-none، آن‌را در تمام اندازه‌ها مخفی می‌کنیم. سپس با افزودن d-xl-inline-block، آن‌را پس از عرض xl نمایان خواهیم کرد.
همین تنظیم را به navbar-brand، در اندازه‌ی sm نیز اضافه کرده‌ایم تا لوگوی سایت در اندازه‌های موبایل ظاهر نشود.



افزودن drop downs به navbar در بوت استرپ 4

برای تبدیل یکی از آیتم‌های منوی راهبری، به منو، از dropdown استفاده می‌شود که نمونه‌ای از آن‌را در مثال زیر مشاهده می‌کنید:
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <a class="navbar-brand d-none d-sm-inline-block" href="#">
                <img src="images/wisdompetlogo.svg" style="width:40px;" alt="">
                Wisdom Pet Medicine
            </a>
            <div class="navbar-nav ml-sm-auto">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>

                <div class="dropdown">
                    <a class="nav-item nav-link dropdown-toggle" data-toggle="dropdown"
                        id="servicesDropdown" aria-haspopup="true"
                        aria-expanded="false" href="#">Services</a>
                    <div class="dropdown-menu" aria-labelledby="servicesDropdown">
                        <a href="#" class="dropdown-item">Grooming</a>
                        <a href="#" class="dropdown-item">General Health</a>
                        <a href="#" class="dropdown-item">Nutrition</a>
                        <a href="#" class="dropdown-item">Pest Control</a>
                        <a href="#" class="dropdown-item">Vaccinations</a>
                    </div>
                </div>

                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
            <span class="navbar-text d-none d-xl-inline-block">The best in
                traditional and alternate
                medicine</span>
        </div>
    </nav>

    <div class="container">
با این خروجی:


- دراپ‌داون نیاز به یک container دارد که آن‌را با تعریف یک div با کلاس dropdown تعریف کرده‌ایم.
- سپس به لینکی که قرار است آن‌را نمایش دهد، کلاس dropdown-toggle را اضافه می‌کنیم تا آیکن مثلثی رو به پایینی را نمایان کند. وجود این مثلث، بیانگر وجود منویی به همراه آن است.
- اکنون با تنظیم data-toggle به dropdown، کدهای جاوا اسکریپتی بوت استرپ، این المان را به صورت یک dropdown پردازش می‌کنند و نیازی به افزودن اسکریپتی به صفحه برای فعالسازی آن نیست. ویژگی‌های aria-expanded و aria-haspopup نیز به مقدار دهی پیش‌فرض‌های کدهای جاوا اسکریپتی آن کمک می‌کنند.
- خود منو توسط دربرگیرنده‌ای با کلاس dropdown-menu و آیتم‌هایی با کلاس dropdown-item تشکیل می‌شود.
- در ادامه برای متصل کردن این دربرگیرنده به لینک نمایش دهنده‌ی منو، یک id را به لینک انتساب می‌دهیم (به نام servicesDropdown) و سپس aria-labelledby دربرگیرنده را به این id، مقدار دهی می‌کنیم.
- در این مثال با استفاده از کلاس navbar-nav ml-sm-auto، سبب شده‌ایم تا منوی سایت، از لبه‌ی سمت راست صفحه پس از عرض sm، شروع شود.


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

برای اضافه کردن المان‌های فرم به منوی راهبری سایت، ابتدا نیاز است کلاس form-inline را بر روی container این فرم قرار داد و سپس به ورودی‌های این فرم، کلاس form-control را اضافه می‌کنیم. اگر نیاز بود، توسط کلاس‌های margin و padding مخصوص بوت استرپ 4 مانند mr-2 نیز می‌توان بین آن‌ها فاصله ایجاد کرد:
<body>
    <nav class="navbar navbar-dark bg-dark navbar-expand-sm">
        <div class="container">
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link" href="#">Testimonials</a>
            </div>
            <form class="form-inline">
                <input type="text" placeholder="Search..." class="form-control mr-2">
                <button class="btn btn-outline-light" type="submit">Go</button>
            </form>
        </div>
    </nav>

    <div class="container">
با این خروجی:


بوت استرپ در اندازه‌ی بزرگتر صفحه، فرم را به سمت راست و آیتم‌های منو را در سمت چپ نمایش می‌دهد.


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

توسط کلاس‌هایی مانند fixed-top (قرار گرفتن در بالای صفحه)، fixed-bottom (قرار گرفتن در پایین صفحه) و sticky-top، می‌توان محل قرارگیری منوی راهبری را تغییر داد. این کلاس‌ها را در مطلب «طرحبندی صفحات وب با بوت استرپ 4 - قسمت دوم» پیشتر بررسی کردیم.
برای توضیح حالت sticky-top، فرض کنید بالای منو، تصویر بزرگی از لوگوی سایت را دارید و این منو زیر آن قرار گرفته‌است. زمانیکه صفحه به سمت پایین اسکرول می‌شود، این منو نیز پایین خواهد آمد تا جائیکه در لبه‌ی بالای صفحه قرار گیرد. پس از آن، این منو در همین ناحیه باقی مانده و شبیه به fixed-top عمل می‌کند.

یک نکته: اگر fixed-bottom را مورد استفاده قرار دادید:
<nav class="navbar navbar-dark bg-dark navbar-expand-sm fixed-bottom">
 ممکن است متن پایین صفحه زیر این منو قرار گیرد و قابل خوانده شدن نباشد. برای این منظور می‌توان از کلاس margin-bottom بر روی container استفاده کرد:
<div class="container mb-5">



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

در مورد کلاس navbar-expand-sm در این مطلب توضیح دادیم. هرچند قابلیت عمودی و افقی شدن خودکار آیتم‌های منوی راهبری بسیار جالب و کاربری است، اما در صفحات نمایشی کوچک، این نمایش عمودی می‌تواند ارتفاع قابل ملاحظه‌ای را به خود اختصاص دهد. به همین جهت می‌خواهیم نمایش آیتم‌های آن‌را وابسته به تصمیم کاربر کنیم.
<body>
    <nav class="navbar navbar-dark bg-dark navbar-expand-sm">
        <div class="container">
            <a href="#" class="navbar-brand">Wisdom Pet Medicine</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#myToggle" aria-controls="myToggle" aria-expanded="false"
                aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="myToggle">
                <div class="navbar-nav">
                    <a class="nav-item nav-link active" href="#">Home</a>
                    <a class="nav-item nav-link" href="#">Mission</a>
                    <a class="nav-item nav-link" href="#">Services</a>
                    <a class="nav-item nav-link" href="#">Staff</a>
                    <a class="nav-item nav-link" href="#">Testimonials</a>
                </div>
            </div>
        </div>
    </nav>

    <div class="container">
- در اینجا نحوه‌ی پیاده سازی منوی همبرگری را در بوت استرپ 4 ملاحظه می‌کنید.
- ابتدا نیاز است دکمه‌ی این منو اضافه شود که توسط کلاس navbar-toggler مشخص شده‌است. سپس با توجه به اینکه این کامپوننت توسط کدهای جاوا اسکریپتی بوت استرپ کار می‌کند، اطلاعات مورد نیاز آن‌را توسط ویژگی‌های data-toggle، data-target و aria مشخص می‌کنیم.
- این دکمه نیاز دارد تا به یک div با کلاس collapse navbar-collapse متصل شود. این اتصال نیز از طریق id آن صورت می‌گیرد که در ویژگی data-target مقدار دهی شده‌است.
- اگر این دکمه را پس از navbar-brand قرار دهیم، در سمت چپ صفحه و اگر پیش از آن قرار دهیم، در سمت راست صفحه ظاهر می‌شود.

در حالت نمایش sm، آیتم‌های منو مخفی شده:


با کلیک بر روی دکمه‌ی منوی همبرگری آن، گزینه‌های منو نمایش داده می‌شوند:


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




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: Bootstrap4_07.zip
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت چهارم - شاخص‌های مهم اطلاعات آماری کوئری‌ها
تا اینجا با روش‌های مختلف جمع آوری اطلاعات آماری مرتبط با کوئری‌های اجرا شده‌ی در SQL Server آشنا شدیم. در این قسمت قصد داریم بررسی کنیم این اطلاعات جمع آوری شده، چه مفاهیمی را در بر دارند و مهم‌ترین‌های آن‌ها کدامند؟


شاخص‌های مهم بررسی کارآیی کوئری‌ها

در ابتدای بررسی هر کوئری، باید 4 شاخص بسیار مهم، مدنظر باشند:
- مدت زمان اجرای کوئری: هرچند بررسی مدت زمان اجرای کوئری، شاخص مهمی‌است، اما الزاما حاوی اطلاعات مفیدی در مورد آن کوئری نیست. برای مثال اگر یک کوئری زیاد طول می‌کشد، حتما به معنای وجود مشکلی با آن نیست؛ ممکن است اطلاعات زیادی را واکشی می‌کند یا ممکن است توسط عاملی سد شده‌است. در این موارد هرچند مشکلاتی وجود دارند، اما مستقیما مرتبط با آن کوئری نیستند.
- میزان مصرف CPU: میزان کاری که باید توسط CPU انجام شود تا کوئری به نتیجه برسد.
- I/O: در SQL Server می‌توان هم physical I/O و هم logical I/O را بررسی کرد. برای مثال اگر اطلاعات مورد درخواست توسط کوئری هم اکنون در حافظه موجود باشند، نیازی به physical I/O پرهزینه نخواهد بود و در مقابل آن logical I/O کم هزینه‌تر است.
- میزان مصرف حافظه

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


مروری بر ابزارهای مختلف اندازه‌گیری شاخص‌های کارآیی

Management studio
درون Management studio می‌توان اطلاعات مرتبط با یک کوئری را به صورت زنده مشاهده کرد. البته این اطلاعات صرفا مرتبط با یک کوئری و یا تعدادی مشخص هستند؛ چون باید کوئری را به صورت دستی درون این برنامه اجرا کرد و سپس اطلاعات اجرای کوئری‌ها را دریافت نمود. اطلاعات آماری که توسط آن نیز ارائه می‌شود محدودیت‌هایی دارد. برای مثال مدت زمان اجرای کوئری و یا تعداد رکوردهای تحت تاثیر قرار گرفته شده را می‌توان مشاهده کرد. اما به اندازه‌ی اطلاعات ارائه شده‌ی در یک execution plan کامل نیست. به علاوه بازگشت اطلاعات حاصل از اجرای کوئری‌ها درون این برنامه، سربار خودش را داشته و سبب کند شدن برنامه می‌شود. در آخر اطلاعات ارائه شده‌ی توسط آن‌را نیز باید از قسمت‌های مختلفی جمع آوری و به صورت دستی ذخیره کرد.

Extended Events
توسط Extended Events نیز می‌توان همانند Management studio، اطلاعات آماری یک تک کوئری و یا یک batch را جمع آوری کرد؛ اما پس از ایجاد و تنظیم آن، به صورت خودکار اجرا می‌شود. در حین تعریف یک سشن Extended Events می‌توان شاخص‌های خاصی را انتخاب کرد و یا شرط‌های دقیقی را اعمال کرد. خروجی آن نیز به صورت خودکار در یک فایل ذخیره می‌شود.

Dynamic management objects
با استفاده از DMO's از نتایج آماری مرتبط با تک کوئری‌ها، به نتایج تجمعی حاصل از اجرای آن‌ها می‌رسیم. این نتایج نیز در plan cache ذخیره می‌شوند. به این معنا که اگر کش، تخلیه (با اجرای دستور DBCC FREEPROCCACHE) و یا سرور ری‌استارت شود، این اطلاعات از دست خواهند رفت. هدف آن بیشتر رفع اشکال کوئری‌هایی است که هم اکنون در حال اجرا هستند. اگر نیاز به اطلاعات دوره‌ای را داشته باشید، نیاز خواهید داشت تا با تهیه‌ی snapshotهایی از بانک اطلاعاتی، این تاریخچه را تکمیل کنید. به همین جهت Query Store ارائه شده‌است تا نیازی به اینکار نباشد.

Query Store
Query Store کار ذخیره سازی متن plan و آمار تجمعی مرتبط با آن‌را به صورت خودکار انجام می‌دهد و آن‌را درون بانک اطلاعاتی کاربر ذخیره می‌کند. به همین جهت با خالی شدن کش، برخلاف DMO's، اطلاعات آن حذف نمی‌شود.


مثالی از روش‌های مختلف جمع آوری اطلاعات آماری حاصل از اجرای کوئری‌ها در SQL Server

در ادامه قصد داریم با مثالی، خلاصه‌ای را از سه قسمتی که تاکنون بررسی کردیم، ارائه دهیم. برای این منظور ابتدا رویه‌ی ذخیره شده‌ی زیر را ایجاد می‌کنیم:
USE [WideWorldImporters];
GO

DROP PROCEDURE IF EXISTS [Application].[usp_GetPersonInfo];
GO

CREATE PROCEDURE [Application].[usp_GetPersonInfo]
    (@PersonID INT)
AS

SELECT
    [p].[FullName],
    [p].[EmailAddress],
    [c].[FormalName]
FROM [Application].[People] [p]
    LEFT OUTER JOIN [Application].[Countries] [c]
    ON [p].[PersonID] = [c].[LastEditedBy]
WHERE [p].[PersonID] = @PersonID;
GO
کار آن دریافت اطلاعات یک کاربر بر اساس ID او می‌باشد.

سپس یک سشن Extended event را با نام QueryPerf ایجاد می‌کنیم:
IF EXISTS (
SELECT *
FROM sys.server_event_sessions
WHERE [name] = 'QueryPerf')
BEGIN
    DROP EVENT SESSION [QueryPerf] ON SERVER;
END
GO

CREATE EVENT SESSION [QueryPerf]
ON SERVER
ADD EVENT sqlserver.sp_statement_completed(
WHERE ([duration]>(1000))),
ADD EVENT sqlserver.sql_statement_completed(
WHERE ([duration]>(1000))),
ADD EVENT sqlserver.query_post_execution_showplan
ADD TARGET package0.event_file(
SET filename=N'C:\Temp\QueryPerf\test.xel',max_file_size=(256))
WITH (
MAX_MEMORY=16384 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,
MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=ON,STARTUP_STATE=OFF);
GO
این سشن به رخ‌دادهای sql_statement_completed، sp_statement_completed و query_post_execution_showplan، اگر طول مدت آن کوئری بیش از 1 میلی ثانیه باشد، واکنش نشان می‌دهد. نتیجه‌ی نهایی را نیز در پوشه‌ی C:\Temp\QueryPerf ذخیره می‌کند (این پوشه را باید به صورت دستی ایجاد کنید).

در ادامه Query Store را نیز بر روی بانک اطلاعاتی WideWorldImporters فعال کرده و همچنین اگر اطلاعاتی از پیش در آن وجود دارند، پاک می‌شود.
USE [master];
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE = ON;
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE (
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30),
DATA_FLUSH_INTERVAL_SECONDS = 60,
INTERVAL_LENGTH_MINUTES = 5,
MAX_STORAGE_SIZE_MB = 100,
QUERY_CAPTURE_MODE = ALL,
SIZE_BASED_CLEANUP_MODE = AUTO,
MAX_PLANS_PER_QUERY = 200);
GO

ALTER DATABASE [WideWorldImporters] SET QUERY_STORE CLEAR;
GO

سپس هر آنچه را که در plan cache نیز وجود دارد، حذف می‌کنیم:
DBCC FREEPROCCACHE;
GO

اکنون سشن QueryPerf را که پیشتر ایجاد کردیم، آغاز می‌کنیم:
ALTER EVENT SESSION [QueryPerf]
ON SERVER
STATE = START;
GO
نتیجه‌ی آن‌را در قسمت management->extended events، با سبز شدن آیکن QueryPerf می‌توانید مشاهده کنید.


در ادامه چون می‌خواهیم نتایج آماری را در management studio نیز مشاهده کنیم، ابتدا جمع آوری شاخص‌های آماری را در یک پنجره‌ی جدید new query، فعال می‌کنیم:
SET STATISTICS IO ON;
GO
SET STATISTICS TIME ON;
GO
SET STATISTICS XML ON;
GO

همچنین در منوی Query، گزینه‌ی Include client statistics را نیز انتخاب می‌کنیم تا مشخص شود که آیا عملیات insert/update/delete انجام شده‌است. چه تعداد ردیف تحت تاثیر اجرای این کوئری قرار گرفته‌اند. چه تعداد تراکنش انجام شده‌است. همچنین اطلاعات آماری شبکه و زمان نیز ارائه شوند.

پس از این تنظیمات، اکنون نوبت به اجرای کوئری‌های زیر رسیده‌است که یکی پارامتری است و دیگری AdHoc:
USE [WideWorldImporters];
GO

EXECUTE [Application].[usp_GetPersonInfo] 1234;
GO

SELECT
    [s].[StateProvinceName],
    [s].[SalesTerritory],
    [s].[LatestRecordedPopulation],
    [s].[StateProvinceCode]
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [c].[CountryName] = 'United States';
GO
با اجرای آن، در management studio، برگه‌های messages و client statistics ظاهر می‌شوند که هر کدام اینبار اطلاعات آماری اجرای این کوئری را به همراه دارند. همچنین در قسمت results، امکان مشاهده‌ی query plan، به علت فعال بودن اطلاعات آماری XML، وجود دارد.




سپس سشن QueryPerf را متوقف و حذف می‌کنیم:
ALTER EVENT SESSION [QueryPerf]
ON SERVER
STATE = STOP;
GO

DROP EVENT SESSION [QueryPerf] ON SERVER;
GO
فایل خروجی با پسوند xel آن را که در پوشه‌ی C:\Temp\QueryPerf ذخیره شده‌است، می‌توان در management studio مشاهده کرد. البته در ابتدای نمایش آن، صرفا دو ستون name و timestamp را نمایش می‌دهد که می‌توان با انتخاب هر ردیف آن و سپس انتخاب و کلیک راست بر روی ردیف‌های details آن، گزینه‌ی Show Column in table را انتخاب کرد تا شاخص مدنظر، در ستون‌های گزارش نیز ظاهر شود.


اگر بخواهیم از عملیات صورت گرفته توسط DMO's کوئری بگیریم:
SELECT
    [qs].[last_execution_time],
    [qs].[execution_count],
    [qs].[total_elapsed_time],
    [qs].[total_elapsed_time]/[qs].[execution_count] [AvgDuration],
    [qs].[total_logical_reads],
    [qs].[total_logical_reads]/[qs].[execution_count] [AvgLogicalReads],
    [t].[text],
    [p].[query_plan]
FROM sys.dm_exec_query_stats [qs]
CROSS APPLY sys.dm_exec_sql_text([qs].sql_handle) [t]
CROSS APPLY sys.dm_exec_query_plan([qs].[plan_handle]) [p]
WHERE [t].[text] LIKE '%Countries%';
GO
که در آن تنها ردیف‌هایی که متن کوئری آن‌ها حاوی Countries است، فیلتر شده، به یک چنین خروجی خواهیم رسید:


همانطور که مشاهده می‌کنید، شاخص‌های چهارگانه‌ای که در ابتدای بحث معرفی شدند، در مورد کوئری پارامتری نوشته شده، وضعیت بسیار بهتری نسبت به کوئری AdHoc دوم دارند.

از Query Store هم می‌توان به صورت زیر کوئری گرفت (علاوه بر قسمت رابط کاربری Query Store که ذیل اشیاء مرتبط با بانک اطلاعاتی WideWorldImporters در management studio قابل مشاهده‌است):
USE [WideWorldImporters];
GO

SELECT
    [qsq].[query_id],
    [qst].[query_sql_text],
    CASE
WHEN [qsq].[object_id] = 0 THEN N'Ad-hoc'
ELSE OBJECT_NAME([qsq].[object_id])
END AS [ObjectName],
    [qsp].[plan_id],
    [rs].[count_executions],
    [rs].[avg_logical_io_reads],
    [rs].[avg_duration],
    TRY_CONVERT(XML, [qsp].[query_plan]),
    [rs].[last_execution_time],
    (DATEADD(MINUTE, -(DATEDIFF(MINUTE, GETDATE(), GETUTCDATE())),
[rs].[last_execution_time])) AS [LocalLastExecutionTime]
FROM [sys].[query_store_query] [qsq]
    JOIN [sys].[query_store_query_text] [qst]
    ON [qsq].[query_text_id] = [qst].[query_text_id]
    JOIN [sys].[query_store_plan] [qsp]
    ON [qsq].[query_id] = [qsp].[query_id]
    JOIN [sys].[query_store_runtime_stats] [rs]
    ON [qsp].[plan_id] = [rs].[plan_id]
WHERE [qst].[query_sql_text] LIKE '%Countries%';
GO