مطالب
چگونه از CodePlex به عنوان مخزنی جهت ذخیره سازی کدهای سایت یا وبلاگ خود استفاده کنیم؟

به شخصه اعتقادی ندارم که جهت مدیریت کار رایگانی که انجام می‌شود از امکانات غیر رایگان استفاده کرد. تابحال برای ذخیره سازی کدهای منتشر شده در این وبلاگ از persiangig تا googlepages مرحوم تا رپیدشیر تا ... استفاده کرده‌ام. نه امکان لیست کردن سریع آن‌ها موجود است و نه مشخص است که چه تعدادی از آن‌ها هنوز وجود خارجی داشته و از سرورهای یاد شده پاک نشده‌اند. اخیرا تعدادی وبلاگ برنامه نویسی را یافته‌ام که از سایت CodePlex به عنوان مخزنی برای ذخیره سازی کدها و مثال‌های منتشر شده در وبلاگ خود استفاده می‌کنند. این کار چند مزیت دارد:
- رایگان است (فضا، پهنای باند، اسکریپت و غیره)
- به صورت تضمینی تا 10 سال دیگر هم پابرجا است.
- درب آن به روی کاربران ایرانی باز است (برخلاف مثلا سایت googlecodes یا رفتار اخیر سورس فورج و غیره، سایت CodePlex در این چندسال رویه ثابتی داشته است)
- امکان مشاهده‌ی لیست تمامی کدهای منتشر شده‌ موجود است.
- امکان ثبت توضیحات کنار هر کد منتشر شده نیز وجود دارد.
- امکان دریافت یکجای آن‌ها با توجه به استفاده از ابزارهای سورس کنترل مهیا است.
- امکان دریافت بهینه‌ی موارد جدید هم برای کاربران وجود دارد. کاربری که یکبار با استفاده از ابزارهای سورس کنترل، کدهای موجود را دریافت کرده، در بار بعدی دریافت اطلاعات، تنها موارد تغییر کرده یا جدید را دریافت خواهد کرد و نه تمام اطلاعات کل مخزن را از ابتدا تا به امروز.
- امکان مشاهده‌ی آمار دریافت‌ها، مراجعات، سایت‌هایی که به شما لینک داده‌اند و غیره فراهم است.
- امکان دعوت کردن از افراد دیگر نیز جهت به روز رسانی مخزن کد تدارک دیده شده است.
- کلیه اعضای CodePlex بدون نیاز به عضویت در گروه مخزن کد شما، می‌توانند جهت تکمیل یا اصلاح کار شما patch یا وصله ارسال کنند.
و ...

اما برای استفاده از این امکانات نیاز است حداقل اطلاعاتی را در مورد کار با ابزارهای سورس کنترل داشت، که خلاصه‌ی مختصر و مفید آن‌را در ادامه ملاحظه خواهید نمود:
0 - دریافت و نصب برنامه‌ی TortoiseSVN
1- ثبت نام در سایت CodePlex
رایگان است.

2- ایجاد یک پروژه‌ی جدید


که به همراه وارد کردن مشخصات اولیه آن است:


تنها نکته‌ی مهم آن انتخاب سورس کنترل Team foundation server و سپس Subversion است چون می‌خواهیم با استفاده از TortoiseSVN کار به روز رسانی اطلاعات را انجام دهیم.

3- انتخاب مجوز برای پروژه در برگه‌ی License پروژه ایجاد شده

تا مجوزی را برای پروژه انتخاب نکنید، مجوز ارائه‌ی عمومی آن‌را نخواهید یافت. در مورد مقایسه‌ی مجوز‌های سورس باز لطفا به این مطلب مراجعه کنید.

4- checkout کردن سورس کنترل
ابتدا به برگه‌ی source code پروژه مراجعه کرده و بر روی لینک subversion در کنار صفحه کلیک کنید.

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


در صفحه‌ی باز شده آدرس svn مربوط به پروژه خود را وارد نموده و بر روی Ok کلیک کنید:



در صفحه‌ی بعدی باید نام کاربری و کلمه‌ی عبور مرتبط با حساب کاربری سایت کدپلکس خود را وارد نمائید. همچنین بهتر است گزینه‌ی به خاطر سپاری آن‌را نیز برای سهولت کار در دفعات بعدی انتخاب کنید:



به این صورت یک پوشه‌ی مخفی svn در اینجا تشکیل خواهد شد که اطلاعات مخزن کد را در خود نگهداری می‌کند و نباید آن‌را حذف کرد، تغییر داد، یا جابجا کرد.



5- اضافه کردن فایل‌های دلخواه به مخزن کد
برای اضافه کردن کدهای مورد نظر خود، آن‌ها را به پوشه‌ی SiteRepository فوق کپی کرده و سپس بر روی آن‌ها کلیک راست نموده و گزینه‌ی Add مربوط به TortoiseSVN را انتخاب کنید:



به این صورت تنها فایل‌های مورد نظر جهت اضافه شدن به مخزن کد علامتگذاری خواهند شد (ایجاد پوشه و قرار دادن فایل‌ها درون آ‌ن‌ها نیز به همین ترتیب است):



اکنون برای تکمیل فرایند، جایی درون پوشه کلیک راست کرده و گزینه‌ی SVN Commit را انتخاب کنید:



در صفحه‌ی باز شده توضیحاتی را در مورد فایل‌های ارسالی وارد کرده و سپس بر روی دکمه‌ی OK کلیک نمائید:



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



همچنین آیکون فایل‌های مورد نظر نیز بر روی کامپیوتر شما به صورت زیر تغییر خواهند کرد:



6- ارائه نهایی پروژه
فراموش نکنید که پس از ایجاد یک پروژه‌ی جدید، انتخاب مجوز و ارسال فایل‌های مورد نظر، باید بر روی دکمه‌ی publish this project در بالای صفحه کلیک کرد. در غیراینصورت پروژه‌ی شما در روز بعد به صورت خودکار از سایت CodePlex حذف می‌گردد:




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


در دفعات آتی، تنها تکرار مرحله 5 یعنی کپی کردن فایل‌های مورد نظر به پوشه‌ی SiteRepository، سپس Add و در نهایت Commit آن‌ها کفایت می‌کند و نیازی به تکرار سایر مراحل نیست. عملیات هماهنگ سازی با مخزن کد هم بسیار بهینه است و تنها فایل‌هایی که اخیرا اضافه شده و هنوز ارسال نشده‌اند، Commit خواهند شد.
کاربران نهایی هم یا از طریق اینترفیس تحت وب سایت می‌توانند از فایل‌های شما استفاده کنند و یا روش دیگری هم برای این منظور وجود دارد (همان Checkout کردن یاد شده و سپس هر بار انتخاب گزینه‌ی SVN update بجای Commit جهت دریافت فایل‌های جدید و نه کل مخزن کد به صورت یکجا).

مطالب
املای صحیح فناوری‌های مرتبط با مایکروسافت

به نظر شما .net درست است یا .Net یا ... ؟
Asp.net چطور؟ آیا ASP.Net درست است یا Asp.net یا ... ؟!
Direct X درست است یا Direct-X یا ... ؟
Linq درست است یا linq یا ...؟

پاسخ: هیچکدام!
املای تمامی موارد ذکر شده نادرست است. جدولی را یکی از کارکنان مایکروسافت در این زمینه تهیه کرده است که در آدرس ذیل می‌توانید مشاهده نمائید:




مطالب
پیاده سازی SoftDelete در EF Core
در مورد حذف منطقی در EF 6x، پیشتر مطالبی را در این سایت مطالعه کرده‌اید:
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامه‌های تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آن‌ها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» می‌کنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقه‌ی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را می‌دهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شده‌اند، اما هنوز در برنامه‌ی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کرده‌است. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف می‌شود.

پیاده سازی حذف منطقی در EF Core شامل مراحل خاصی است که در این مطلب، جزئیات آن‌ها را بررسی خواهیم کرد.


نیاز به تعریف دو خاصیت جدید در هر جدول

هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. می‌توان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا می‌توان ابتدا یک کلاس پایه‌ی abstract را برای آن ایجاد کرد:
using System;

namespace EFCoreSoftDelete.Entities
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }


        public bool IsDeleted { set; get; }
        public DateTime? DeletedAt { set; get; }
    }
}
و سپس موجودیت‌هایی را که قرار است از soft delete پشتیبانی کنند، توسط آن علامتگذاری کرد؛ مانند موجودیت Blog:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Blog : BaseEntity
    {
        public string Name { set; get; }

        public virtual ICollection<Post> Posts { set; get; }
    }

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.Property(blog => blog.Name).HasMaxLength(450).IsRequired();
            builder.HasIndex(blog => blog.Name).IsUnique();

            builder.HasData(new Blog { Id = 1, Name = "Blog 1" });
            builder.HasData(new Blog { Id = 2, Name = "Blog 2" });
            builder.HasData(new Blog { Id = 3, Name = "Blog 3" });
        }
    }
}
که هر بلاگ از تعدادی مطلب تشکیل شده‌است:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Post : BaseEntity
    {
        public string Title { set; get; }

        public Blog Blog { set; get; }
        public int BlogId { set; get; }
    }

    public class PostConfiguration : IEntityTypeConfiguration<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.Property(post => post.Title).HasMaxLength(450);
            builder.HasOne(post => post.Blog).WithMany(blog => blog.Posts).HasForeignKey(post => post.BlogId);

            builder.HasData(new Post { Id = 1, BlogId = 1, Title = "Post 1" });
            builder.HasData(new Post { Id = 2, BlogId = 1, Title = "Post 2" });
            builder.HasData(new Post { Id = 3, BlogId = 1, Title = "Post 3" });
            builder.HasData(new Post { Id = 4, BlogId = 1, Title = "Post 4" });
            builder.HasData(new Post { Id = 5, BlogId = 2, Title = "Post 5" });
        }
    }
}
مزیت علامتگذاری این کلاس‌ها، امکان کوئری گرفتن از آن‌ها نیز می‌باشد که در ادامه از آن استفاده خواهیم کرد.


حذف خودکار رکوردهایی که Soft Delete شده‌اند، از نتیجه‌ی کوئری‌ها و گزارشات

تا اینجا فقط دو خاصیت ساده را به کلاس‌های مدنظر خود اضافه کرده‌ایم. پس از آن یا می‌توان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائه‌ی EF Core 2x می‌توان برای آن‌ها Query Filter تعریف کرد. برای مثال می‌توان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئری‌های مرتبط با این موجودیت‌ها اعمال کرد:
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            // ...
            builder.HasQueryFilter(blog => !blog.IsDeleted);
        }
    }
از این پس ذکر context.Blogs دقیقا معنای context.Blogs.Where(blog => !blog.IsDeleted) را می‌دهد و دیگر نیازی به ذکر صریح شرط متناظر با soft delete نیست.
در این حالت کوئری‌های نهایی به صورت خودکار دارای شرط زیر خواهند شد:
SELECT [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[IsDeleted] <> CAST(1 AS bit)


اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیت‌ها

همانطور که عنوان شد، مزیت علامتگذاری موجودیت‌ها با کلاس پایه‌ی BaseEntity، امکان کوئری گرفتن از آن‌ها است:
namespace EFCoreSoftDelete.DataLayer
{
    public static class GlobalFiltersManager
    {
        public static void ApplySoftDeleteQueryFilters(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model
                                                    .GetEntityTypes()
                                                    .Where(eType => typeof(BaseEntity).IsAssignableFrom(eType.ClrType)))
            {
                entityType.addSoftDeleteQueryFilter();
            }
        }

        private static void addSoftDeleteQueryFilter(this IMutableEntityType entityData)
        {
            var methodToCall = typeof(GlobalFiltersManager)
                                .GetMethod(nameof(getSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
                                .MakeGenericMethod(entityData.ClrType);
            var filter = methodToCall.Invoke(null, new object[] { });
            entityData.SetQueryFilter((LambdaExpression)filter);
        }

        private static LambdaExpression getSoftDeleteFilter<TEntity>() where TEntity : BaseEntity
        {
            return (Expression<Func<TEntity, bool>>)(entity => !entity.IsDeleted);
        }
    }
}
در اینجا در ابتدا تمام موجودیت‌هایی که از BaseEntity ارث بری کرده‌اند، یافت می‌شوند. سپس بر روی آن‌ها قرار است متد SetQueryFilter فراخوانی شود. این متد بر اساس تعاریف EF Core، یک LambdaExpression کلی را قبول می‌کند که نمونه‌ی آن در متد getSoftDeleteFilter تعریف شده و سپس توسط متد addSoftDeleteQueryFilter به صورت پویا به modelBuilder اعمال می‌شود.

محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیت‌های موجود اعمال شود:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {

        //...


        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseEntity).Assembly);
            modelBuilder.ApplySoftDeleteQueryFilters();
        }


مشکل! هنوز هم حذف فیزیکی رخ می‌دهد!

تنظیمات فوق، تنها بر روی کوئری‌های نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ می‌دهد.
 برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آن‌را تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
namespace EFCoreSoftDelete.DataLayer
{
    public static class AuditableEntitiesManager
    {
        public static void SetAuditableEntityOnBeforeSaveChanges(this ApplicationDbContext context)
        {
            var now = DateTime.UtcNow;

            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //TODO: ...
                        break;
                    case EntityState.Modified:
                        //TODO: ...
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Unchanged; //NOTE: For soft-deletes to work with the original `Remove` method.

                        entry.Entity.IsDeleted = true;
                        entry.Entity.DeletedAt = now;
                        break;
                }
            }
        }
    }
}
در اینجا با استفاده از سیستم tracking، رکوردهای حذف شده‌ی با وضعیت EntityState.Deleted، به وضعیت EntityState.Unchanged تغییر پیدا می‌کنند، تا دیگر حذف نشوند. اما در ادامه چون دو خاصیت IsDeleted و DeletedAt این موجودیت، ویرایش می‌شوند، وضعیت جدید Modified خواهد بود که به کوئری‌های Update تفسیر می‌شوند. به این ترتیب می‌توان همانند قبل یک رکورد را حذف کرد:
var post1 = context.Posts.Find(1);
if (post1 != null)
{
   context.Remove(post1);

   context.SaveChanges();
}
اما دستوری که توسط EF Core صادر می‌شود، یک Update است:
Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:11:32' (Nullable = true), @p1='True'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            ChangeTracker.DetectChanges();

            beforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges(acceptAllChangesOnSuccess);

            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        // ...

        private void beforeSaveTriggers()
        {
            setAuditProperties();
        }

        private void setAuditProperties()
        {
            this.SetAuditableEntityOnBeforeSaveChanges();
        }
    }
}


مشکل! رکوردهای وابسته حذف نمی‌شوند!

حالت پیش‌فرض حذف رکوردها در EFCore به cascade delete تنظیم شده‌است. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابسته‌ی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
 ar blog1 = context.Blogs.FirstOrDefault(blog => blog.Id == 1);
if (blog1 != null)
{
   context.Remove(blog1);

   context.SaveChanges();
}
تنها تک رکورد متناظر با آن حذف منطقی شده و مطالب متناظر با آن خیر. برای رفع این مشکل باید به صورت زیر عمل کرد:
var blog1AndItsRelatedPosts = context.Blogs
    .Include(blog => blog.Posts)
    .FirstOrDefault(blog => blog.Id == 1);
if (blog1AndItsRelatedPosts != null)
{
    context.Remove(blog1AndItsRelatedPosts);

    context.SaveChanges();
}
ابتدا باید رکوردهای وابسته را توسط یک Include به حافظه وارد کرد و سپس دستور Delete را بر روی کل آن صادر نمود که یک چنین خروجی را تولید می‌کند:
SELECT [t].[Id], [t].[DeletedAt], [t].[IsDeleted], [t].[Name], [t0].[Id], [t0].[BlogId], [t0].[DeletedAt], [t0].[IsDeleted], [t0].[Title]
FROM (
SELECT TOP(1) [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE ([b].[IsDeleted] <> CAST(1 AS bit)) AND ([b].[Id] = 1)
) AS [t]
LEFT JOIN (
SELECT [p].[Id], [p].[BlogId], [p].[DeletedAt], [p].[IsDeleted], [p].[Title]
FROM [Posts] AS [p]
WHERE [p].[IsDeleted] <> CAST(1 AS bit)
) AS [t0] ON [t].[Id] = [t0].[BlogId]
ORDER BY [t].[Id], [t0].[Id]

Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:25:00' (Nullable = true), @p1='True',
 @p5='2', @p3='2020-09-17T05:25:00' (Nullable = true), @p4='True', @p8='3',
@p6='2020-09-17T05:25:00' (Nullable = true), @p7='True',
 @p11='4', @p9='2020-09-17T05:25:00' (Nullable = true), @p10='True'], CommandType='Text', CommandTimeout='30']

SET NOCOUNT ON;
UPDATE [Blogs] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p3, [IsDeleted] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p6, [IsDeleted] = @p7
WHERE [Id] = @p8;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p9, [IsDeleted] = @p10
WHERE [Id] = @p11;
SELECT @@ROWCOUNT;
ابتدا اولین بلاگ را حذف منطقی کرده؛ سپس تمام مطالب متناظر با آن‌را که پیشتر حذف منطقی نشده‌اند، یکی یکی به صورت حذف شده، علامتگذاری می‌کند. به این ترتیب cascade delete منطقی نیز در اینجا میسر می‌شود.


یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد

فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کرده‌اید و اکنون رکوردی در این بین، حذف منطقی شده‌است. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شده‌است؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده می‌کنید، از قابلیتی به نام filtered indexes پشتیبانی می‌کند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکس‌ها وجود دارد. در این حالت می‌توان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreSoftDelete.zip
مطالب
MVVM و فراخوانی متدهای اشیاء View از طریق ViewModel

ما در ViewModel دسترسی مستقیمی به هیچ یک از اشیاء موجود در View نداریم (و درستش هم همین است). الان فرض کنید که می‌خواهیم از طریق ViewModel یک View را ببندیم؛ مثلا متد Close آن پنجره را فراخوانی کنیم. به عبارتی در حالت کلی می‌خواهیم یکی از متدهای تعریف شده یکی از عناصر بصری موجود در View را از طریق ViewModel فراخوانی نمائیم.
برای حل این مساله از فایل‌های همان SDK‌ مرتبط با Expression blend استفاده خواهیم کرد.

ابتدا ارجاعاتی را به اسمبلی‌های System.Windows.Interactivity.dll و Microsoft.Expression.Interactions.dll اضافه می‌کنیم.
سپس دو فضای نام مرتبط هم باید اضافه شوند:

  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"  
  xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"

یک مثال عملی:
قصد داریم از طریق ViewModel ، پنجره‌ای را ببندیم. کدهای XAML این مثال را در ادامه مشاهده خواهید کرد:

<Window x:Class="WpfCallMethodActionSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:vm="clr-namespace:WpfCallMethodActionSample.ViewModels"
Name="ThisWindow"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">

<Button Content="Save &amp; Close" VerticalAlignment="Top" Margin="5">
<i:Interaction.Triggers>
<!--فراخوانی متدی در ویوو مدل-->
<i:EventTrigger EventName="Click">
<ei:CallMethodAction
TargetObject="{Binding}"
MethodName="SaveButtonClicked" />
</i:EventTrigger>

<!--فراخوانی متدی در شیء جاری از طریق ویوو مدل-->
<i:EventTrigger SourceObject="{Binding}" EventName="CloseMainWindow">
<ei:CallMethodAction
TargetObject="{Binding ElementName=ThisWindow}"
MethodName="Close"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
</Window>

همچنین ViewModel تعریف شده نیز همین چند سطر زیر است:

using System;

namespace WpfCallMethodActionSample.ViewModels
{
public class MainWindowViewModel
{
public void SaveButtonClicked()
{
close();
}

public event EventHandler CloseMainWindow;
private void close()
{
if (CloseMainWindow != null) CloseMainWindow(this, EventArgs.Empty);
}
}
}

توضیحات:
اگر به ViewModel دقت کنید خبری از DelegateCommand در آن نیست. بله، به کمک ترکیبی از EventTrigger و CallMethodAction می‌توان جایگزینی را جهت DelegateCommand معرفی شده در قسمت‌های قبل این سری مباحث MVVM ارائه داد.
EventTrigger در اینجا به این معنا است که اگر EventName ذکر شده رخ داد، آنگاه این اعمال را انجام بده. مثلا در اینجا CallMethodAction را فراخوانی کن.
CallMethodAction در اسمبلی Microsoft.Expression.Interactions.dll تعریف شده است و تنها متدی از نوع void و بدون پارامتر را می‌تواند به صورت خودکار فراخوانی کند (محدودیت مهم آن است).
اینکه این متد کجا قرار دارد، توسط TargetObject آن مشخص می‌شود. اگر TargetObject را مساوی Binding قرار دادیم، یعنی به دنبال متدی که در DataContext گرید وجود دارد بگرد. به عبارتی به صورت خودکار به SaveButtonClicked تعریف شده در ViewModel ما متصل خواهد شد و آن‌را فراخوانی می‌کند.

تا اینجا رخداد Click دکمه تعریف شده را به متد SaveButtonClicked موجود در ViewModel سیم کشی کردیم.

در مرحله بعد می‌خواهیم از طریق ViewModel ، متدی را در View فراخوانی کنیم. نکته آن هم پیشتر ذکر شد؛ TargetObject صحیحی را باید انتخاب کرد. در اینجا برای پنجره جاری نام ThisWindow تعریف شده است و از طریق تعریف:

TargetObject="{Binding ElementName=ThisWindow}"

به CallMethodAction خواهیم گفت که قرار است متد Close را در شیء ThisWindow فراخوانی کنی.
همچنین نحوه تعریف EventTrigger ما هم در اینجا برعکس شده است:

<i:EventTrigger SourceObject="{Binding}"  EventName="CloseMainWindow">

قبلا به دنبال مثلا رخداد Click یک دکمه بودیم، اکنون با توجه به SourceObject تعریف شده، در ViewModel به دنبال این رخداد که برای نمونه در اینجا CloseMainWindow نام گرفته خواهیم گشت.

بنابراین View اینبار به رخداد CloseMainWindow تعریف شده در ViewModel سیم کشی خواهد شد. اکنون اگر این رخداد در ViewModel فراخوانی شود، CallMethodAction متناظر فعال شده و متد Close پنجره را فراخوانی می‌کند.

مطالب
ASP.NET MVC #4

بررسی نحوه ارتباطات بین اجزای مختلف الگوی MVC در ASP.NET MVC

اینبار برخلاف قسمت قبل، قالب پروژه خالی ASP.NET MVC را در VS.NET انتخاب کرده (ASP.NET MVC 3 Web Application و بعد انتخاب قالب Empty نمایش داده شده) و سپس پروژه جدیدی را شروع می‌کنیم. View Engine را هم Razor در نظر خواهیم گرفت.
پس از ایجاد ساختار اولیه پروژه، بدون اعمال هیچ تغییری، برنامه را اجرا کنید. بلافاصله با پیغام The resource cannot be found یا 404 یافت نشد، مواجه خواهیم شد.
همانطور که در پایان قسمت دوم نیز ذکر شد، پردازش‌ها در ASP.NET MVC از کنترلرها شروع می‌شوند و نه از صفحات وب. بنابراین برای رفع این مشکل نیاز است تا یک کلاس کنترلر جدید را اضافه کنیم. به همین جهت بر روی پوشه استاندارد Controllers کلیک راست کرده، از منوی ظاهر شده قسمت Add، گزینه‌ی Controller را انتخاب کنید:


در صفحه بعدی که ظاهر می‌شود، نام HomeController را وارد کنید (با توجه به اینکه مطابق قراردادهای ASP.NET MVC، نام کنترلر باید به کلمه Controller ختم شود). البته لازم به ذکر است که این مراحل را به همان شکل متداول مراجعه به منوی Project و انتخاب Add Class و سپس ارث بری از کلاس Controller نیز می‌توان انجام داد و طی این مراحل الزامی نیست. کلاسی که به صورت خودکار از طریق منوی Add Controller یاد شده ایجاد می‌شود، به شکل زیر است:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}

سؤال:
در قسمت دوم عنوان شد که کنترلر باید کلاسی باشد که اینترفیس IController را پیاده سازی کرده است، اما در اینجا ارث بری از کلاس Controller را شاهد هستیم. جریان چیست؟!
سلسله مراتبی که بکارگرفته شده به صورت زیر است:
public abstract class ControllerBase : IController
public abstract class Controller : ControllerBase

ControllerBase، اینترفیس IController را پیاده سازی کرده و سپس کلاس Controller از کلاس ControllerBase مشتق شده است. شاید بپرسید که این همه پیچ و تاب برای چیست؟!
مشکلی که با اینترفیس خالص وجود دارد، عدم نگارش پذیری آن است. به این معنا که اگر متدی یا خاصیتی در نگارش بعدی به این اینترفیس اضافه شد، هیچکدام از پروژه‌های قدیمی دیگر کامپایل نخواهند شد و باید ابتدا این متد یا خاصیت جدید را نیز لحاظ کنند. اینجا است که کار کلاس‌های abstract شروع می‌شود. در یک کلاس abstract می‌توان پیاده سازی پیش فرضی را نیز ارائه داد. به این ترتیب مصرف کننده نهایی کلاس Controller متوجه این تغییرات نخواهد شد.
اگر برنامه نویس «من» باشم، شما رو وادار خواهم کرد که متدهای جدید اینترفیس تعریفی‌ام را پیاده سازی کنید! همینه که هست! اما اگر طراح مایکروسافت باشد، بلافاصله انبوهی از جماعت ایرادگیر که بالای 100 تا از کنترلر‌های اون‌ها الان فقط در یک پروژه از کار افتاده،‌ ممکن است جلوی دفتر مایکروسافت دست به خود سوزی بزنند! اینجا است که مایکروسافت مجبور است تا این پیچ و تاب‌ها را اعمال کند که اگر روزی متدی در اینترفیس IController بنابر نیازهای جدید درنظر گرفته شد، بتوان سریع یک پیاده سازی پیش فرض از آن‌را در کلاس‌های abstract یاد شده قرار داد (یکی از تفاوت‌های مهم کلاس‌های abstract با اینترفیس‌ها) تا جماعت ایرادگیر و نق‌زن متوجه تغییری نشوند و باز هم پروژه‌های قدیمی بدون مشکل کامپایل شوند. تابحال به فلسفه وجودی کلاس‌های abstract از این دیدگاه فکر کرده بودید؟!

بعد از این توضیحات و کارها، اگر اینبار برنامه را اجرا کنیم، خطای زیر نمایش داده می‌شود:

The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Home/Index.aspx
~/Views/Home/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/Home/Index.cshtml
~/Views/Home/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml

همانطور که ملاحظه می‌کنید چون نام کنترلر ما Home است، فریم ورک در پوشه استاندارد Views در زیر پوشه‌ای به همان نام Home، به دنبال یک سری فایل می‌گردد. فایل‌های aspx مربوط به View Engine ایی به همین نام بوده و فایل‌های cshtml و vbhtml مربوط به View Engine دیگری به نام Razor هستند.
بنابراین نیاز است تا یکی از این فایل‌ها را در مکان‌های یاد شده ایجاد کنیم. برای این منظور حداقل دو راه وجود دارد. یا دستی اینکار را انجام دهیم یا اینکه از ابزار توکار خود VS.NET برای ایجاد یک View جدید استفاده کنیم.
برای ایجاد View ایی مرتبط با متد Index (در ASP.NET MVC نام دیگر متدهای قرار گرفته در یک کنترلر، Action نیز می‌باشد)، روی خود متد کلیک راست کرده و گزینه Add View را انتخاب کنید:


در صفحه بعدی ظاهر شده، پیش فرض‌ها را پذیرفته و بر روی دکمه Add کلیک نمائید. اتفاقی که رخ خواهد داد شامل ایجاد فایل Index.cshtml، با محتوای زیر است (با توجه به اینکه زبان پروژه سی شارپ است و View Engine انتخابی Razor می‌باشد، cshtml تولید گردید و گرنه vbhtml ایجاد می‌شد):

@{
ViewBag.Title = "Index";
}
<h2>Index</h2>

مجددا برنامه را اجرا کنید. اینبار بدون خطایی کلمه Index را در صفحه تولیدی می‌توان مشاهده کرد. نکته جالب این فایل‌های View جدید، عدم مشاهده ویژگی‌های runat=server و سایر موارد مشابه است.

چند سؤال مهم:
در حین ایجاد اولین کنترلر جهت نمایش صفحه پیش فرض برنامه، نام HomeController انتخاب شد. چرا مثلا نام TestController وارد نشد؟ برنامه از کجا متوجه شد که باید حتما این کنترلر را پردازش کند. نقش متد Index چیست؟ آیا حتما باید Index باشد و در اینجا نام دیگری را نمی‌توان وارد کرد؟ «قرارداد» پردازشی این‌ها کجا تنظیم می‌شود؟ فریم ورک، این سیم کشی‌ها را چگونه انجام می‌دهد؟
پاسخ به تمام این سؤال‌ها، در ویژگی «مسیریابی یا Routing» نهفته است. فایل Global.asax.cs برنامه را باز کنید. تعاریف مرتبط با مسیریابی پیش فرض را در متد RegisterRoutes آن می‌توان مشاهده کرد:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}



پروسه هدایت یک درخواست HTTP به یک کنترلر، در اینجا مسیریابی یا Routing نامیده می‌شود. این قابلیت در فضای نام System.Web.Routing تعریف شده است و باید دقت داشت که جزو ASP.NET MVC نیست. این امکانات جزو ASP.NET Runtime است و به همراه دات نت 3.5 سرویس پک یک برای اولین بار ارائه شد. بنابراین جهت استفاده در ASP.NET Web forms نیز مهیا است. در ASP.NET MVC از این امکانات برای ارسال درخواست‌ها به کنترلرها استفاده می‌شود.
در متد routes.MapRoute فراخوانی شده‌ای که در کدهای بالا ملاحظه می‌کنید، کار نگاشت یک URL به Actionهای یک کنترلر صورت می‌گیرد (یا همان متدهای تعریف شده در کنترلرها). همچنین از این URLها پارامترهای این متدها یا اکشن‌ها نیز قابل استخراج است.
در متد MapRoute، اولین پارامتر تعریف شده، یک نام پیش فرض است و در ادامه اگر آدرسی را به فرم «یک چیزی اسلش یک چیزی اسلش یک چیزی» یافت، اولین قسمت آن‌را به عنوان نام کنترلر تفسیر خواهد کرد، دومین قسمت آن، نام متد عمومی موجود در کنترلر فرض شده و سومین قسمت به عنوان پارامتر ارسالی به این متد پردازش می‌شود.
برای مثال از آدرس زیر اینطور می‌توان دریافت که:
http://hostname/home/about

Home نام کنترلی است که فریم ورک به دنبال آن خواهد گشت تا این درخواست رسیده را پردازش کند و about نام متدی عمومی در این کلاس است که به صورت خودکار فراخوانی می‌گردد. در اینجا پارامتر id ایی هم وجود ندارد. در یک برنامه امکان تعریف چندین و چند مسیریابی وجود دارد.
پارامتر سوم متد routes.MapRoute، یک سری پیش فرض را تعریف می‌کند و این مورد همانجایی است که از اطلاعات آن جهت تعریف کنترلر پیش فرض استفاده کردیم. برای مثال به چهار آدرس زیر دقت نمائید:

http://localhost/
http://localhost/home
http://localhost/home/about
http://localhost/process/list

در حالت http://localhost/،‌ هر سه مقدار پیش فرض مورد استفاده قرار خواهند گرفت چون سه جزئی ({controller}/{action}/{id}) که موتور مسیریابی به دنبال آن‌ها می‌گردد، در این آدرس وجود خارجی ندارد. بنابراین نام کنترلر پیش فرض در این حالت همان Home مشخص شده در پارامتر سوم متد routes.MapRoute خواهد بود و متد پیش فرض نیز Index و پارامتری هم به آن ارسال نخواهد شد.
در حالت http://localhost/home نام کنترلر مشخص است اما دو جزء دیگر ذکر نشده‌اند، بنابراین مقادیر آن‌ها از پیش فرض‌های صریح ذکر شده در متد routes.MapRoute گرفته می‌شود. یعنی نام متد اکشن مورد پردازش، Index خواهد بود به همراه هیچ آرگومان خاصی.
به علاوه باید خاطرنشان کرد که این مقادیر case sensitive یا حساس به بزرگی و کوچکی حروف نیستند. بنابراین مهم نیست که http://localhost/HoMe باشد یا http://localhost/HOMe یا هر ترکیب دیگری.
یا اگر آدرس رسیده http://localhost/process/list باشد، این مسیریابی پیش فرض تعریف شده قادر به پردازش آن می‌باشد. به این معنا که کنترلری به نام Process وهله سازی و سپس متدی به نام List در آن فراخوانی خواهند شد.

یک نکته:
ترتیب routes.MapRouteهای تعریف شده در اینجا مهم است و اگر اولین URL رسیده با الگوی تعریف شده مطابقت داشت، کار را تمام خواهد کرد و به سایر تعاریف نخواهد رسید. مثلا اگر در اینجا یک مسیریابی دیگر را به نام Process تعریف کنیم:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Process", // Route name
"Process/{action}/{id}", // URL with parameters
new { controller = "Process", action = "List", id = UrlParameter.Optional } // Parameter defaults
);

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}

آدرسی به فرم http://localhost/Process، به صورت خودکار به کنترلر Process و متد عمومی List آن نگاشت خواهد شد و کار به مسیریابی بعدی نخواهد رسید.


مطالب
کوئری هایی با قابلیت استفاده ی مجدد
با توجه به اصل Dry تا می‌توان باید از نوشتن کدهای تکراری خودداری کرد و کد‌ها را تا جایی که ممکن است به قسمت هایی با قابلیت استفاده‌ی مجدد تبدیل کرد. حین کار کردن با ORM‌های معروف مثل NHibernate و EntityFramework زمان زیادی نوشتن کوئری‌ها جهت واکشی داده‌ها از دیتابیس صرف می‌شود. اگر بتوان کوئری هایی با قابلیت استفاده‌ی مجدد نوشت علاوه بر کاهش زمان توسعه قابلیت هایی قدرتمندی مانند زنجیر کردن کوئری‌ها به دنبال هم به دست می‌آید. 
با یک مثال نحوه‌ی نوشتن و مزایای کوئری با قابلیت استفاده‌ی مجدد را بررسی می‌کنیم : 
برای مثال دو جدول شهر‌ها و دانش آموزان را درنظر بگیرید:
namespace ReUsableQueries.Model
{
    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    [ForeignKey("BornInCityId")]
        public virtual City BornInCity { get; set; }
        public int BornInCityId { get; set; }
    }

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

        public virtual ICollection<Student> Students { get; set; }
    }
}
در ادامه این کلاس‌ها را در معرض دید EF Code first قرار داده:
using System.Data.Entity;
using ReUsableQueries.Model;

namespace ReUsableQueries.DAL
{
    public class MyContext : DbContext
    {
        public DbSet<City> Cities { get; set; }
        public DbSet<Student> Students { get; set; }
    }
}

و همچنین تعدادی رکورد آغازین را نیز به جداول مرتبط اضافه می‌کنیم:
    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }
        protected override void Seed(MyContext context)
        {
            var city1 = new City { Name = "city-1" };
            var city2 = new City { Name = "city-2" };
            context.Cities.Add(city1);
            context.Cities.Add(city2);
            var student1 = new Student() {Name = "Shaahin",LastName = "Kiassat",Age=22,BornInCity = city1};
            var student2 = new Student() { Name = "Mehdi", LastName = "Farzad", Age = 31, BornInCity = city1 };
            var student3 = new Student() { Name = "James", LastName = "Hetfield", Age = 49, BornInCity = city2 };
            context.Students.Add(student1);
            context.Students.Add(student2);
            context.Students.Add(student3);
            base.Seed(context);
        }
    }
فرض کنید قرار است یک کوئری نوشته شود که در جدول دانش آموزان بر اساس نام ، نام خانوادگی و سن جستجو کند : 
         var context = new MyContext();
          var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where(
              x => x.Age == age);
احتمالا هنوز کسانی هستند که فکر می‌کنند کوئری‌های LINQ همان لحظه که تعریف می‌شوند اجرا می‌شوند اما اینگونه نیست . در واقع این کوئری فقط یک Expression از رکورد‌های جستجو شده است و تا زمانی که متد ToList یا ToArray روی آن اجرا نشود هیچ داده ای برگردانده نمی‌شود.
در یک برنامه‌ی واقعی داده‌های باید به صورت صفحه بندی شده و مرتب شده برگردانده شود پس کوئری به این صورت خواهد بود : 
        var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where(
              x => x.Age == age).OrderBy(x=>x.LastName).Skip(skip).Take(take);
ممکن است بخواهیم در متد دیگری در لیست دانش آموزان بر اساس نام ، نام خانوادگی ، سن و شهر جستجو کنیم و سپس خروجی را اینبار بر اساس سن مرتب کرده و صفحه بندی نکنیم:
          var query = context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where
              (
                  x => x.Age == age).Where(x => x.BornInCityId == 1).OrderBy(x => x.Age);
همانطور که می‌بینید قسمت هایی از این کوئری با کوئری هایی که قبلا نوشتیم یکی است ، همچنین حتی ممکن است در قسمت دیگری از برنامه نتیجه‌ی همین کوئری را به صورت صفحه بندی شده لازم داشته باشیم.
اکنون نوشتن این کوئری‌ها میان کد های Business Logic باعث شده هیچ استفاده‌ی مجددی نتوانیم از این کوئری‌ها داشته باشیم. حال بررسی می‌کنیم که چگونه می‌توان کوئری هایی با قابلیت استفاده‌ی مجدد نوشت : 
namespace ReUsableQueries.Quries
{
    public static class StudentQueryExtension
    {
        public static IQueryable<Student> FindStudentsByName(this IQueryable<Student> students,string name)
        {
            return students.Where(x => x.Name.Contains(name));
        }
        public static IQueryable<Student> FindStudentsByLastName(this IQueryable<Student> students, string lastName)
        {
            return students.Where(x => x.LastName.Contains(lastName));
        }
        public static IQueryable<Student> SkipAndTake(this IQueryable<Student> students, int skip , int take)
        {
            return students.Skip(skip).Take(take);
        }
        public static IQueryable<Student> OrderByAge(this IQueryable<Student> students)
        {
            return students.OrderBy(x=>x.Age);
        }
    }
}
همان طور که مشاهده می‌کنید به کمک متد‌های الحاقی برای شیء IQueryable<Student> چند کوئری نوشته ایم . اکنون در محل استفاده از کوئری‌ها می‌توان این کوئری‌ها را به راحتی به هم زنجیر کرد. همچنین اگر روزی قرار شد منطق یکی از کوئری‌ها عوض شود با عوض کردن آن در یک قسمت برنامه همه جا اعمال می‌شود.  نحوه‌ی استفاده از این متدهای الحاقی به این صورت خواهد بود : 
     var query = context.Students.FindStudentsByName(name).FindStudentsByLastName(lastName).SkipAndTake(skip,take);          
فرض کنید قرار است یک سیستم جستجوی پیشرفته به برنامه اضافه شود که بر اساس شرط‌های مختلف باید یک شرط در کوئری اعمال شود یا نشود ، به کمک این طراحی جدید به راحتی می‌توان بر اساس شرط‌های مختلف یک کوئری را اعمال کرد یا نکرد : 
        var query = context.Students.AsQueryable();
          if (searchByName)
          {
              query= query.FindStudentsByName(name);
          }
          if (orderByAge)
          {
              query = query.OrderByAge();
          }
          if (paging)
          {
             query =  query.SkipAndTake(skip, take);
          }
          return query.ToList();
همچنین این کوئری‌ها وابسته به ORM خاصی نیستند البته این نکته هم مد نظر است که LINQ Provider بعضی ORM‌ها ممکن است بعضی کوئری‌ها را پشتیبانی نکند.

نظرات مطالب
اهمیت Controller های ساده در ASP.NET MVC
Controller فقط مصرف کننده‌ی منطق نهایی است ، بدنه‌ی کنترلر‌ها جای مناسبی برای پیاده سازی منطق تجاری نیست. BL که شما از آن یاد می‌کنید در لایه‌ی دیگری رخ می‌دهد ، یکی اسم آم را Task می‌گذارد ، یکی Service و دیگری BlaBla ..
MVC جایگزینی برای N-Tier  نیست ، بلکه روشی برای سازماندهی لایه‌ی نمایش می‌باشد.
ViewModel‌ها  (ViewModel === Model Of View) اشیایی هستند که از طریق لایه‌ی سرویس تولید می‌شوند و در واقع داده ای که باید در View به کاربر نمایش داده شود را نگهداری می‌کنند.
نگرانی و مسئولیت Controller فراهم کردن داده  (از طریق اجرای Business logic) برای UI می‌باشد.
مطالب
آموزش LINQ بخش سوم
در ادامه سری آموزشی LINQ  به بررسی متغیرهای Range می‌پردازیم:
4 عنصر یک عبارت پرس و جو عبارتند از:
• علملگرهای LINQ
• کلمات کلیدی Keyword
• متغیر‌های Range

Range Variable : متغیر تعریف شده‌ی در یک محدوده خاص.

  عبارت پرس و جوی زیر را در نظر بگیرد:
var query = from word in list
where word.StartsWith("a")
select word;
در این پرس و جو (from,in,where,select) کلمات کلیدی محسوب می‌شوند. البته where و select می‌توانند به عنوان عملگر محسوب شوند.
شناسه‌ی list یک متغیر محلی است و تنها موردی که باقی می‌ماند شناسه‌ی word است که به آن متغیر Range می‌گوییم. متغیر‌های Range همانند متغیر‌های مرسوم مورد استفاده‌ی در برنامه‌ها هستند که بصورت فقط خواندنی مهیا شده اند. با این اوصاف متغیر‌های Range در ابتدا کمی عجیب به نظر می‌رسند. به این علت که در وسط عبارت پرس و جو معرفی می‌شوند و نیازی به تعریف شدن به روش مرسوم به شکل زیر را ندارند:
 String word;
در این حالت معرفی Word از طریق عبارت from انجام می‌شود. کامپایلر نوع داده‌ی متغیر را از طریق فرآیندی به نام Type Inference مشخص می‌کند. در مثال بالا کامپایلر تشخیص می‌دهد که Word از نوع string  است؛ به این علت که جدا شده از <list<string  می‌باشد.

متغیر word  در دو حالت ممکن است قابل دسترس نباشد :
  • پایان پرس و جو
  • مواجه شدن با کلمه کلیدی into . این کلمه‌ی کلیدی برای اتصال دو Query استفاده می‌شود.

نکته
:در بعضی مواقع باید وضعیت متغیر Range را صریحا مشخص کنیم؛ بطور مثال کد زیر با خطا مواجه خواهد شد:
object[] ints = new object[] { 1, 2, 3 };
           var query = from num in ints
                        where num < 3
                        select num;
در زمان پردازش دستور Where، کامپایلر نمی‌تواند عملگر مقایسه‌ای را برای یک نوع int و یک نوع object اجرا کند. برای حل این مشکل بصورت صریح (Explicit) نوع متغیر Range را مشخص می‌کنیم:
var query = from int num in ints
where num < 3
select num;
همانطور که می‌بینید در این حالت به کامپایلر اعلام می‌کنیم که num از نوع int می‌باشد و cast کردن با موفقیت انجام می‌شود و خروجی همان چیزی است که ما انتظار داریم.

تذکر : بهتر است از تعریف صریح متغیر Range پرهیز کنیم؛ مگر در شرایطی مثل کد بالا .

قطعه کد زیر به‌راحتی کامپایل می‌شود و نیازی به اعلان صریح نوع متغیر range نیست. زیر از طریق مکانیزیم Type Inference نوع متغیر مشخص شده است.
List<string> list = new List<string> {"LINQ","Query","adventure"};
var query = from string word in list
where word.Contains("r")
orderby word ascending
select word;
اعلان صریح متغیر Range باعث می‌شود که پشت پرده، عملیات <Cast<T اتفاق بیافتد و در مواقع غیر ضروری مثل کد فوق ممکن است کارآیی را کاهش دهد. یکی از نقاطی که در صورت پایین بودن کارآیی دستورات LINQ باید بررسی شود همین مورد CAST است. البته تنها استثنایی که در این مورد وجود دارد، توالی‌های غیر جنریک هستند (non generic Enumerable). در این حالت باید از Cast استفاده کرد.
متغیر Range در محدوده‌ی مورد استفاده باید از یک شناسه‌ی یکتا برخوردار باشد.
 string word="test";
List<string> list = new List<string> {"LINQ","Query","adventure"};
var query = from string word in list
where word.Contains("r")
orderby word ascending
select word;
همانطور که مشاهده می‌کنید کامپایلر خطای تعریف دو شناسه‌ی یکتا را در یک محدوده، اعلام می‌کند.

تا اینجا از طریق کلمه‌ی کلیدی from، متغیری را تعریف کردیم. با استفاده از کلمات کلیدی let ،into و join  نیز می‌توان متغیر‌های Range تعریف کرد.

عبارت let

کلمه‌ی کلیدی let این امکان را فراهم می‌کند تا یک متغیر Range جدید را ایجاد کرده و در عبارت‌های بعدی از آن استفاده کنیم. در کد زیر از طریق کلمه‌ی کلیدی let، یک متغیر Range جدید را بنام IsDairy تعریف می‌کنیم که از نوع bool  می‌باشد:
 Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 200}
};

IEnumerable<Ingredient> highCalDairyQuery =
from i in ingredients
let isDairy = i.Name == "Milk" || i.Name == "Butter"
where i.Calories >= 150 && isDairy
select i;

foreach (var ingredient in highCalDairyQuery)
{
   Console.WriteLine(ingredient.Name);
}
  متغیر isDairy در عبارت (clause) بعدی که where باشد، مورد استفاده قرار گرفته است. توجه داشته باشید که متغیر i  تعریف شده‌ی در ابتدای پرس و جو، در بخش Select قابل دسترسی است. دستور let باعث از دسترس خارج شدن متغیر در بخش بعدی نمی‌شود.

در کد زیر قصد داریم عملیات‌های زیر را بر روی توالی ورودی اعمال کنیم:
1- جدا کردن عناصر توالی ورودی بر اساس جدا کننده‌ی "  ," 
2- تبدیل همه‌ی حروف عناصر توالی ایجاد شده به حروف بزرگ
3- جدا کردن عناصر توالی حاصل از مرحله‌ی 2، به شرط برابر بودن با MILK,BUTTER,CHEESE
4- نمایش توالی ایجاد شده
string[] csvRecipes = { "milk,sugar,eggs", "flour,BUTTER,eggs", "vanilla,ChEEsE,oats" };
var dairyQuery = from csvRecipe in csvRecipes
let ingredients = csvRecipe.Split(',')
from ingredient in ingredients
let uppercaseIngredient = ingredient.ToUpper()
where
  uppercaseIngredient == "MILK" ||
  uppercaseIngredient == "BUTTER" ||
  uppercaseIngredient == "CHEESE"
select uppercaseIngredient;

foreach (var dairyIngredient in dairyQuery)
{
   Console.WriteLine($"{dairyIngredient} is dairy");
}
همانطور که مشاهده می‌کنید متغیر ایجاد شده‌ی توسط let می‌تواند یک مقدار عددی (مثال قبل) و یا یک مجموعه را در خود ذخیره کند (مثال فوق).

 عبارت Into 
متغیر جدیدی که توسط این دستور ایجاد می‌شود، می‌تواند نتیجه‌ی حاصل از دستور Select را در خود ذخیره کند. در کد زیر یک نوع بی‌نام ایجاد کرده و در ادامه‌ی پرس و جو از آن استفاده می‌کنیم:
 Ingredient[] ingredients =
{
   new Ingredient {Name = "Sugar", Calories = 500},
   new Ingredient {Name = "Egg", Calories = 100},
   new Ingredient {Name = "Milk", Calories = 150},
   new Ingredient {Name = "Flour", Calories = 50},
   new Ingredient {Name = "Butter", Calories = 200}
};

IEnumerable<Ingredient> highCalDairyQuery =
from i in ingredients
select new //نوع بی نام
{
  OriginalIngredient = i,
  IsDairy = i.Name == "Milk" || i.Name == "Butter",
  IsHighCalorie = i.Calories >= 150
}
into temp
where temp.IsDairy && temp.IsHighCalorie
select temp.OriginalIngredient;

foreach (var ingredient in highCalDairyQuery)
{
   Console.WriteLine(ingredient.Name);
}
نکته‌ای که در ابتدای این بحث اشاره شد، در این مثال خود را نشان می‌دهد و آن هم عدم دسترسی به متغیر i در بخش پایانی پرس و جو (select  نهایی) می‌باشد.

در ادامه‌ی این سری آموزشی، به بررسی عبارت join  می‌پردازیم. 
نظرات مطالب
بارگذاری یک یوزرکنترل با استفاده از جی‌کوئری
سلام استاد نصیری
بسیار مقاله مفید و Trick جالبی بود.به تازگی افزونه ای برای فایر باگ عرضه شده به اسم Fire Query که مواردی را برای کار و دیباگ jQuery فراهم میکنه.
یه سوال از حضورتون داشتم چرا خروجی Execute در وب سرویس شامل تگ های html و body نیست؟آیا این همیشه ثابته؟ و اینکه مواردی مثل r\ و n\ رو هم از خروجی حذف کنیم بهتر نیست؟
باسپاس از شما استاد عزیز
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت اول - جمع آوری اطلاعات آماری کوئری‌های زنده
بسیاری از شرکت‌ها دارای نقشی مانند «مدیران بانک اطلاعاتی» نیستند؛ اما تعدادی «توسعه دهنده‌ی بانک‌های اطلاعاتی» را به همراه دارند که گاهی از اوقات از آن‌ها خواسته می‌شود تا کارآیی پایین کوئری‌های صورت گرفته را بررسی و رفع کنند و ... آن‌ها دقیقا نمی‌دانند که باید از کجا شروع کنند! فقط می‌دانند که یک کوئری، مدت زمان زیادی را برای اجرا به خود اختصاص می‌دهد؛ اما نمی‌دانند که چگونه باید به کوئری پلن آن دسترسی یافت و چگونه باید آن‌را تفسیر کرد. در این حالت حداکثر کاری را که ممکن است انجام دهند، افزودن یک ایندکس جدید است که ممکن است کار کند و یا خیر و حتی اگر کار کند، دقیقا نمی‌دانند که چگونه! هدف از این سری، بررسی مقدماتی روش‌های بهبود کارآیی کوئری‌ها در SQL Server، از دید یک «توسعه دهنده‌ی بانک‌های اطلاعاتی» است.


پیشنیازهای این سری

در این سری از بانک اطلاعاتی استاندارد مثال به همراه SQL Server 2016، به نام WideWorldImporters استفاده می‌کنیم. برای دریافت آن، به قسمت releases مثال‌های مایکروسافت مراجعه کرده و فایل WideWorldImporters-Full.bak را دریافت کنید. پس از دریافت این فایل، برای restore سریع آن، می‌توانید دستور زیر را اجرا کنید که در آن باید مسیر فایل bak دریافتی و همچنین مسیر ایجاد فایل‌های mdf/ldf/ndf را مطابق مسیرهای سیستم خودتان اصلاح نمائید (فقط مسیر پوشه‌ها را نیاز است تغییر دهید):
use master;

RESTORE DATABASE WideWorldImporters 
FROM disk='D:\path\WideWorldImporters-Full.bak'
WITH MOVE 'WWI_Primary' TO 'D:\SQL_Data\WideWorldImporters.mdf',
MOVE 'WWI_Log' TO 'D:\SQL_Data\WideWorldImporters_log.ldf',
MOVE 'WWI_UserData' TO 'D:\SQL_Data\WideWorldImporters_UserData.ndf',
MOVE 'WWI_InMemory_Data_1' TO 'D:\SQL_Data\WideWorldImporters_InMemory_Data_1'
همچنین صرفنظر از نگارش SQL Server ای که در حال استفاده‌ی از آن هستید (البته به حداقل SQL Server 2016 نیاز خواهید داشت)، بهتر است آخرین نگارش برنامه‌ی management studio را نیز به صورت مستقل دریافت و نصب کنید که در این زمان نگارش 18.1 است.


یافتن اطلاعاتی در مورد کوئری‌ها

SQL Server زمانیکه یک کوئری را اجرا می‌کند، اطلاعاتی را نیز به همراه آن تولید خواهد کرد که سبب ایجاد یک Query Plan می‌شود و در آن، اطلاعاتی مانند جداول مورد استفاده، نوع جوین‌ها، ایندکس‌های استفاده شده و غیره وجود دارند. علاوه بر آن، Query Statistics نیز قابل دسترسی هستند که در آن مدت زمان اجرای یک کوئری، میزان I/O صورت گرفته و میزان مصرف CPU کوئری، ذکر می‌شوند. برای دسترسی یافتن به این اطلاعات، می‌توان به اشیاء مختلف SQL Server مراجعه کرد؛ مانند dynamic management objects یا به اختصار DMO's، همچنین extended events، traces، query stores و یا حتی management studio. مهم‌ترین تفاوت این‌ها نیز در نحوه‌ی دسترسی به اطلاعات آن‌ها است که می‌تواند زنده (live) و یا ذخیره شده در جائی باشند. در اینجا تنها منبعی که امکان مشاهده‌ی این اطلاعات را به صورت زنده میسر می‌کند، management studio است. البته live در اینجا به معنای امکان مشاهده‌ی تمام اطلاعات مرتبط با یک کوئری، مانند آمار و کوئری پلن آن در داخل محیط management studio، پس از اجرای یک کوئری است. در این قسمت بیشتر به روش استخراج اطلاعات آماری کوئری‌های زنده می‌پردازیم و در قسمت‌های بعدی، سایر گزینه‌های نامبرده شده را نیز بررسی خواهیم کرد.


مشاهده‌ی زنده‌ی داده‌های مرتبط با اجرای یک کوئری در management studio

پس از restore بانک اطلاعاتی مثال WideWorldImporters که عنوان شد، در برنامه‌ی Microsoft SQL Server Management Studio، کوئری زیر را اجرا می‌کنیم:
USE [WideWorldImporters];
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 دقت کنیم، مشخص کرده‌است که این کوئری، 53 ردیف را بازگشت داده و همچنین کمتر از 1 ثانیه مدت زمان اجرای آن، طول کشیده‌است:


اینجا است که نیاز به اطلاعات بیشتری در مورد نحوه‌ی اجرای این کوئری داریم. برای استخراج این اطلاعات، اینبار گزینه‌های تولید و جمع آوری اطلاعات آماری IO و TIME را روشن می‌کنیم و سپس همان کوئری قبلی را اجرا خواهیم کرد:
USE [WideWorldImporters];
GO

SET STATISTICS IO ON;
GO
SET STATISTICS TIME ON;
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
ظاهر اجرای این کوئری با کوئری قبلی، تفاوت خاصی ندارد. اما اگر در همینجا به برگه‌ی messages، که در کنار برگه‌ی results و نمایش ردیف‌ها قرار دارد، مراجعه کنیم، یک چنین خروجی قابل مشاهده است:
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 504 ms.

(53 rows affected)
Table 'Countries'. Scan count 0, logical reads 118, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'StateProvinces'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 10 ms.
در اینجا اطلاعات آماری مدت زمان کامپایل و همچنین مدت زمان اجرای کوئری، ارائه شده‌اند. به علاوه در میانه‌ی این آمار، اطلاعات IO کوئری مانند logical reads درج شده‌اند.


استخراج اطلاعات Actual Execution Plan یک کوئری

کوئری را زیر با فرض IO ON و TIME ON حاصل از اجرای کوئری قبل، اجرا می‌کنیم:
USE [WideWorldImporters];
GO

SET STATISTICS XML ON;
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

SET STATISTICS XML OFF;
GO
با فعالسازی اطلاعات آماری XML (و خاموش کردن آن در انتهای کار)، اینبار در برگه‌ی messages، اطلاعات بیشتری ارائه شده‌اند:
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 7 ms.

(53 rows affected)
Table 'Countries'. Scan count 0, logical reads 118, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'StateProvinces'. Scan count 1, logical reads 43, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row affected)

 SQL Server Execution Times:
   CPU time = 15 ms,  elapsed time = 179 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 0 ms.

 SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
اگر دقت کنید اینبار زمان اجرا اندکی بیشتر شده‌است؛ چون درخواست تهیه‌ی query plan را داده‌ایم. این plan را در ذیل قسمت نتایج کوئری می‌توان مشاهده کرد:


اگر بر روی این XML کلیک کنیم، برگه‌ی جدید نمایش گرافیکی این plan ظاهر می‌شود:


با کلیک راست بر روی این برگه، می‌توان اطلاعات آن‌را جهت بررسی‌های بعدی و یا به اشتراک گذاری آن ذخیره کرد.
در این plan اگر اشاره‌گر ماوس را بر روی هر کدام از عناصر آن حرکت دهیم، اطلاعاتی مانند actual number of rows نیز مشاهده می‌شود، در کنار اطلاعات تخمینی؛ به همین جهت به آن Actual Execution Plan هم گفته می‌شود.


این یک روش دسترسی به Execution Plan است. روش دوم آن با استفاده از امکانات رابط کاربری خود Management Studio است؛ با فشردن دکمه‌های Ctrl+M و یا انتخاب گزینه‌ی Include actual execution plan از منوی Query آن. پس از آن کوئری زیر را اجرا کنید:
SET STATISTICS IO ON;
GO
SET STATISTICS TIME ON;
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
اینبار در برگه‌ی نتایج کوئری، برگه‌ی سوم Execution Plan قابل مشاهده‌است:




استخراج اطلاعات Estimated Execution Plan یک کوئری

تا اینجا نحوه‌ی استخراج اطلاعات Actual Execution Plan را بررسی کردیم که به همراه اطلاعات دقیق حاصل از اجرای کوئری نیز بود؛ مانند actual number of rows. نوع دیگری از Execution Planها را نیز می‌توان از SQL Server درخواست کرد که به آن‌ها Estimated Execution Plan گفته می‌شود و حاصل اجرای کوئری نیستند؛ بلکه تخمینی هستند از روش اجرای این کوئری توسط SQL Server. برای فعالسازی محاسبه‌ی آن، ابتدا کوئری زیر را در management studio انتخاب کنید:
USE [WideWorldImporters];
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
سپس از منوی Query، گزینه‌ی Display estimated execution plan را انتخاب نمائید و یا دکمه‌های Ctrl+L را فشار دهید. در این حالت برگه‌های حاصل، حاوی قسمت results نیستند؛ چون کوئری اجرا نشده‌است. اما هنوز برگه‌ی Execution Plan قابل مشاهده است:


همانطور که مشاهده می‌کنید، اینبار نتیجه‌ی حاصل، به همراه اطلاعاتی مانند actual number of rows نیست و صرفا تخمینی است از روش اجرای این کوئری، توسط SQL Server.


جمع آوری اطلاعات آماری کلاینت‌ها

در منوی Query، گزینه‌ای تحت عنوان Include client statistics نیز وجود دارد. با انتخاب آن، اگر کوئری زیر را اجرا کنیم:
USE [WideWorldImporters];
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
اینبار برگه‌ی جدید client statistics ظاهر می‌شود:


در اینجا مشخص می‌شود که آیا عملیات insert/update/delete انجام شده‌است. چه تعداد ردیف تحت تاثیر اجرای این کوئری قرار گرفته‌اند. چه تعداد تراکنش انجام شده‌است. همچنین اطلاعات آماری شبکه و زمان نیز در اینجا ارائه شده‌اند.
در همین حالت، کوئری جدید زیر را با تغییر قسمت where کوئری قبلی، اجرا کنید:
SELECT
    [s].[StateProvinceName],
    [s].[SalesTerritory],
    [s].[LatestRecordedPopulation],
    [s].[StateProvinceCode]
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [s].[StateProvinceName] LIKE 'O%';
GO
نتیجه‌ی آن، ظاهر شدن ستون جدید trial 2 است که می‌تواند جهت مقایسه‌ی کوئری‌های مختلف با هم، بسیار مفید باشد:


در اینجا حداکثر 10 کوئری را می‌توان با هم مقایسه کرد و بیشتر از آن سبب حذف موارد قدیمی از لیست می‌شود.


عدم نمایش ردیف‌های بازگشت داده شده‌ی توسط کوئری در حین جمع آوری اطلاعات آماری

هربار اجرای یک کوئری در management studio، به همراه بازگشت و نمایش ردیف‌های مرتبط با آن کوئری نیز می‌باشد. اگر می‌خواهید در حین بررسی کارآیی کوئری‌ها از نمایش این ردیف‌ها صرف نظر کنید (تا بار این برنامه کاهش یابد)، می‌توانید از منوی Query، گزینه‌ی Query Options را انتخاب کرده و در قسمت Results، گزینه‌ی Grid آن، گزینه‌ی discard results after execution را انتخاب کنید تا دیگر برگه‌ی results نمایش داده نشود و وقت و منابع را تلف نکند. بدیهی است پس از پایان کار بررسی آماری، نیاز به عدم انتخاب این گزینه خواهد بود.