مطالب
ساخت یک گزارش ساز به کمک iTextSharp و Open Office

iTextSharp پایه کار با فایل‌های PDF را ارائه می‌دهد اما ابزاری را جهت ساده‌تر سازی تولید فایل‌های PDF به همراه ندارد؛ هر چند مثلا امکان تبدیل HTML به PDF را دارا است اما باید گفت: «تا حدودی البته». اگر نیاز باشد جدولی را ایجاد کنیم باید کد نویسی کرد، اگر نیاز باشد تصویری اضافه شود به همین ترتیب و الی آخر. البته این را هم باید در نظر داشت که کد نویسی انعطاف قابل توجهی را در اختیار برنامه نویس قرار می‌دهد؛ شاید به همین دلیل این روزها مباحث «Code first» بیشتر مورد توجه برنامه نویس‌ها است، تا مباحث «Wizard first» یک دهه قبل!
اما باز هم داشتن یک طراح بد نیست و می‌تواند در کاهش مدت زمان تولید نهایی یک فایل PDF مؤثر باشد. برای این منظور می‌توان از برنامه‌ی رایگان و معروف Open office استفاده کرد. توسط آن می‌توان یک فرم PDF را طراحی و سپس فیلدهای آن‌را (این قالب تهیه شده را) با iTextSharp پر کرد. این مورد می‌تواند برای تهیه گزارش‌هایی که تهیه آن‌ها با ابزارهای متداول گزارش سازی عموما میسر نیست،‌ بسیار مناسب باشد.


طراحی یک فرم PDF با استفاده از برنامه Open Office

آخرین نگارش برنامه Open office را از اینجا می‌توانید دریافت کنید و آنچنان حجمی هم ندارد؛ حدودا 154 مگابایت است.
پس از نصب و اجرای برنامه، حداقل به دو طریق می‌توان یک فرم جدید را شروع کرد:
الف) آغاز یک XML Form document جدید در Open office سبب خواهد شد که نوارهای ابزار طراحی فرم، مانند قرار دادن TextBox ، CheckBox و غیره به صورت خودکار ظاهر شوند.
ب) و یا آغاز یک سند معمولی و سپس مراجعه به منوی View->Toolbars->Form Controls هم همان حالت را به همراه خواهد داشت.


در اینجا برای طراحی یک گزارش یا فرم جدید تنها کافی است همانند روش‌های متداول تهیه یک سند معمولی رفتار کنیم و مواردی را که قرار است توسط iTextSharp مقدار دهی کنیم، با کنترل‌های نوار ابزار Form آن بر روی صفحه قرار دهیم که نمونه‌ی ساده آن‌را در شکل زیر ملاحظه می‌کنید:


برای گزارش‌های فارسی بهتر است Alignment یک کنترل به Right تنظیم شود و Border به حالت Without frame مقدار دهی گردد. نام این کنترل را هم بخاطر بسپارید و یا تغییر دهید. از این نام‌ها در iTextSharp استفاده خواهیم کرد. (صفحه خواص فوق با دوبار کلیک بر روی یک کنترل قرار گرفته بر روی فرم ظاهر می‌شود)

مرحله بعد، تبدیل این فرم به فایل PDF است. کلیک بر روی دکمه تهیه خروجی به صورت PDF در نوار ابزار اصلی آن برای اینکار کفایت می‌کند. این گزینه در منوی File نیز موجود است.


فرم‌های PDF تهیه شده در اینجا، فقط خواندنی هستند. مثلا یک کاربر می‌تواند آن‌ها را پر کرده و چاپ کند. اما ما از آن‌ها در ادامه به عنوان قالب گزارشات استفاده خواهیم کرد. بنابراین جهت ویرایش فرم‌های تهیه شده بهتر است فایل‌های اصلی Open Office مرتبط را نیز درجایی نگهداری کرد و هر بار پس از ویرایش، نیاز است تا خروجی جدید PDF آن‌ها تهیه شود.


استفاده از iTextSharp جهت مقدار دهی فیلدهای یک فرم PDF

در ادامه می‌خواهیم این قالب گزارشی را که تهیه کردیم با کمک iTextSharp پر کرده و یک فایل PDF جدید تهیه کنیم. سورس کامل اینکار را در ذیل مشاهده می‌کنید:


using System;
using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace PdfForm
{
class Program
{
//روش صحیح ثبت و معرفی فونت در این کتابخانه
public static iTextSharp.text.Font GetTahoma()
{
var fontName = "Tahoma";
if (!FontFactory.IsRegistered(fontName))
{
var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
FontFactory.Register(fontPath);
}
return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
}

static void Main(string[] args)
{
string fileNameExisting = @"form.pdf";
string fileNameNew = @"newform.pdf";

using (var existingFileStream = new FileStream(fileNameExisting, FileMode.Open))
using (var newFileStream = new FileStream(fileNameNew, FileMode.Create))
{
var pdfReader = new PdfReader(existingFileStream);
using (var stamper = new PdfStamper(pdfReader, newFileStream))
{
//نکته مهم جهت کار با اطلاعات فارسی
//در غیراینصورت شاهد ثبت اطلاعات نخواهید بود
stamper.AcroFields.AddSubstitutionFont(GetTahoma().BaseFont);

//form.Fields.Keys = تمام فیلدهای موجود در فرم
var form = stamper.AcroFields;

//مقدار دهی فیلدهای فرم
form.SetField("TextBox1", "مقدار1");
form.SetField("TextBox2", "مقدار2");

// "Yes" and "Off" are valid values here
form.SetField("Check Box 1", "Yes");

// "" and "Off" are valid values here
form.SetField("Option Button 1", "");

// نحوه مقدار دهی لیست
form.SetListOption("ListBox1", new[] { "1مقدار یک", "مقدار دو1" }, null);
form.SetField("ListBox1", null);

// به این ترتیب فرم دیگر توسط کاربر قابل ویرایش نخواهد بود
//stamper.PartialFormFlattening --> جهت غیرقابل ویرایش نمودن فیلدی مشخص
stamper.FormFlattening = true;

stamper.Close();
pdfReader.Close();
}
}

Process.Start("newform.pdf");
}
}
}

توضیحات:
چون در اینجا فایل PDF، از پیش تهیه شده است، پس باید از اشیاء PdfReader و PdfStamper جهت خواندن و نوشتن اطلاعات در آن‌ها استفاده کرد. سپس توسط شیء stamper.AcroFields می‌توان به این فیلدها یا همان کنترل‌هایی که در برنامه‌ی Open office بر روی فرم قرار دادیم، دسترسی پیدا کنیم.
در ابتدا نیاز است فونت این فیلدها توسط متد AddSubstitutionFont مقدار دهی شود. این مورد برای گزارش‌های فارسی الزامی است؛ در غیراینصورت متنی را در خروجی مشاهده نخواهید کرد.
ادامه کار هم مشخص است. توسط متد form.SetField مقداری را به کنترل‌های قرار گرفته بر روی فرم نسبت می‌دهیم. آرگومان اول آن نام کنترل است و آرگومان دوم، مقدار مورد نظر می‌باشد. اگر کنترل CheckBox را بر روی صفحه قرار دادید، تنها مقدارهای Yes و Off را می‌پذیرد (آن هم با توجه به اینکه به کوچکی و بزرگی حروف حساس است). اگر یک Radio button یا در اینجا Option button را بر روی فرم قرار دادید، تنها مقدارهای خالی و Off را قبول خواهد کرد. نحوه‌ی مقدار دهی یک لیست هم در اینجا ذکر شده است.
در پایان چون نمی‌خواهیم کاربر نهایی قادر به ویرایش اطلاعات باشد، FormFlattening را true خواهیم کرد و به این ترتیب، کنترل‌ها فقط خواندنی خواهند شد. البته اگر همانطور که ذکر شد، border کنترل‌ها را در حین طراحی حذف کنید، PDF نهایی تولیدی یکپارچه و یک دست به نظر می‌رسد و اصلا مشخص نخواهد بود که این فایل پیشتر یک فرم قابل پر کردن بوده است.

مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت سوم - بهبود کیفیت جستجوهای FTS توسط یک غلط یاب املایی
فرض کنید کاربری برای جستجوی رکورد زیر:
context.Chapters.Add(new Chapter
{
    Title = "آزمایش متن فارسی",
    Text = "برای نمونه تهیه شده‌است",
    User = user1.Entity
});
بجای «فارسی»، واژه‌ی «فارشی» را وارد کند و یا بجای «آزمایش»، بنویسد «آزمایس». در هر دو حالت نتیجه‌ی جستجوی او خروجی را به همراه نخواهد داشت. برای بهبود تجربه‌ی کاربری جستجوی تمام متنی SQLite، افزونه‌ای به نام spell fix1 برای آن تهیه شده‌است که بر اساس توکن‌های ایندکس شده‌ی FTS، یک واژه‌نامه، تشکیل می‌شود و سپس بر اساس الگوریتم‌های غلط‌یابی املایی آن، از این توکن‌های از پیش موجود که واقعا در فیلدهای متنی بانک اطلاعاتی جاری وجود خارجی دارند، نزدیک‌ترین واژه‌های ممکن را پیشنهاد می‌کند تا بتوان بر اساس آن‌ها، جستجوی دقیق‌تری را ارائه کرد.


کامپایل افزونه‌ی spell fix1

افزونه‌ی spell fix، به همراه هیچکدام از توزیع‌های باینری SQLite ارائه نمی‌شود. ارائه‌ی آن فقط به صورت سورس کد است و باید خودتان آن‌را کامپایل کنید!


برای این منظور ابتدا به آدرس https://www.sqlite.org/src/dir?ci=99749d4fd4930ccf&name=ext/misc مراجعه کرده و فایل ext/misc/spellfix.c آن‌را دریافت کنید. اگر بر روی لینک spellfix.c کلیک کنید، در نوار ابزار بالای صفحه‌ی بعدی، لینک download آن هم وجود دارد.

سپس به صفحه‌ی دریافت اصلی SQLite یعنی https://www.sqlite.org/download.html مراجعه کرده و بسته‌ی amalgamation آن‌را دریافت کنید. این بسته به همراه کدهای اصلی SQLite است که باید در کنار افزونه‌های آن قرار گیرند تا بتوان این افزونه‌ها را کامپایل کرد. بنابراین پس از دریافت بسته‌ی amalgamation و گشودن آن، فایل spellfix.c را به داخل پوشه‌ی آن کپی کنید:


اکنون نوبت به کامپایل فایل spellfix.c و تبدیل آن به یک dll است تا بتوان آن‌را به صورت یک افزونه در برنامه بارگذاری کرد. برای این منظور از هر کامپایلر ++C ای می‌توانید استفاده کنید. برای نمونه به آدرس http://www.codeblocks.org/downloads/binaries مراجعه کرده و بسته‌ی codeblocks-20.03mingw-setup.exe را دریافت کنید (بسته‌ای که به همراه mingw است). پس از نصب آن، در مسیر C:\Program Files (x86)\CodeBlocks\MinGW\bin می‌توانید کامپایلر چندسکویی gcc را مشاهده کنید. توسط آن می‌توان با اجرای دستور زیر، سبب تولید فایل spellfix1.dll شد:
 "C:\Program Files (x86)\CodeBlocks\MinGW\bin\gcc.exe" -g -shared -fPIC -Wall D:\path\to\sqlite-amalgamation-3310100\spellfix.c -o spellfix1.dll


روش معرفی افزونه‌های SQLite به Microsoft.Data.Sqlite

EF Core، از بسته‌ی Microsoft.Data.Sqlite در پشت صحنه برای کار با SQLite استفاده می‌کند و در اینجا هم برای معرفی افزونه‌ی کامپایل شده، باید ابتدا آن‌را به اتصال برقرار شده، معرفی کرد. خود Sqlite در ویندوز، افزونه‌هایش را بر اساس معرفی مستقیم مسیر فایل dll آن‌ها بارگذاری نمی‌کند. بلکه path ویندوز را برای جستجوی آن‌ها بررسی کرده و در صورتیکه فایل dll ای را افزونه تشخیص داد، آن‌را بارگذاری می‌کند. بنابراین یا باید به صورت دستی مسیر فایل dll تولید شده را به متغیر محیطی path ویندوز اضافه کرد و یا می‌توان توسط قطعه کد زیر، آن‌را به صورت پویایی معرفی کرد:
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class LoadSqliteExtensions
    {
        public static void AddToSystemPath(string extensionsDirectory)
        {
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                throw new NotSupportedException("Modifying the path at runtime only works on Windows. On Linux and Mac, set LD_LIBRARY_PATH or DYLD_LIBRARY_PATH before running the app.");
            }

            var path = new HashSet<string>(Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator));
            if (path.Add(extensionsDirectory))
            {
                Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, path));
            }
        }
    }
}
در این متد extensionsDirectory، همان پوشه‌ای است که فایل dll کامپایل شده، در آن قرار دارد. مابقی آن، معرفی این مسیر به صورت پویا به PATH سیستم عامل است.

در ادامه پیش از معرفی services.AddDbContext، باید مسیر پوشه‌ی افزونه‌ها را ثبت کرد و سپس UseSqlite را به همراه اتصالی استفاده کرد که توسط متد LoadExtension آن، افزونه‌ی spellfix1 به آن معرفی شده‌است:
LoadSqliteExtensions.AddToSystemPath("path to .dll file");
services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) =>
    {
        var connection = new SqliteConnection(connectionString);
        connection.Open();

        connection.LoadExtension("spellfix1");
        // Passing in an already open connection will keep the connection open between requests.
        optionsBuilder.UseSqlite(connection);
    });
همانطور که عنوان شد، متد LoadExtension، مسیری را دریافت نمی‌کند. این متد فقط نام افزونه را دریافت می‌کند و مسیر آن‌را از PATH سیستم عامل می‌خواند.


ایجاد جداول ویژه‌ی spell fix در برنامه

در قسمت اول، با متد createFtsTables آشنا شدیم. اکنون این متد را برای ایجاد جداول کمکی مرتبط با افزونه‌ی spell fix به صورت زیر تکمیل می‌کنیم:
        private static void createFtsTables(ApplicationDbContext context)
        {
            // For SQLite FTS
            // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too.
            context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS""
                                    USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");");

            // 'SQLite Error 1: 'no such module: spellfix1'.' --> must be loaded ...
            // EditCost for unicode support
            context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_Vocab USING fts5vocab('Chapters_FTS', 'row');");
            context.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Chapters_FTS_SpellFix_EditCost(iLang INT, cFrom TEXT, cTo TEXT, iCost INT);");
            context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_SpellFix USING spellfix1(edit_cost_table=Chapters_FTS_SpellFix_EditCost);");
        }
- اگر در حین اجرای این دستورات خطای «no such module: spellfix1» را دریافت کردید، یعنی متد LoadExtension را به درستی فراخوانی نکرده‌اید.
- همانطور که مشاهده می‌کنید، ابتدا بر اساس Chapters_FTS یا همان جدول مجازی FTS برنامه، یک جدول مجازی از نوع fts5vocab ایجاد می‌شود. کار آن استخراج توکن‌های FTS و آماده سازی آن‌ها برای استفاده در غلط یاب املایی هستند.
- سپس جدول ویژه‌ی EditCost را مشاهده می‌کنید. نام آن مهم نیست، اما ساختار آن باید دقیقا به همین صورت باشد. اگر این جدول اختیاری را تهیه کنیم، الگوریتم spellfix1 به utf8 سوئیچ خواهد کرد و برای پردازش متون یونیکد، بدون مشکل کار می‌کند. بدون آن، جستجوهای فارسی نتایج مطلوبی را به همراه نخواهند داشت.
- در آخر جدول مجازی مرتبط با spellfix1 که از جدول cost_table معرفی شده استفاده می‌کند، ایجاد شده‌است.

اجرای این دستورات، جداول زیر را ایجاد می‌کنند (که ساختار آن‌ها استاندارد است و باید مطابق فرمول‌های مستندات آن‌ها باشد):



به روز رسانی جدول واژه نامه‌ی غلط یابی برنامه

آخرین جدولی را که ایجاد کردیم، Chapters_FTS_SpellFix است که اطلاعات خودش را از Chapters_FTS_Vocab دریافت می‌کند:


  هر بار که بانک اطلاعاتی را به روز می‌کنیم، نیاز است اطلاعات این جدول را نیز توسط دستور زیر به روز کرد:
database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS_SpellFix(word, rank)
    SELECT term, cnt FROM Chapters_FTS_Vocab
    WHERE term not in (SELECT word from Chapters_FTS_SpellFix_vocab)");
البته خود SQLite اطلاعات این جدول را فقط یکبار بارگذاری می‌کند. برای اجبار آن به بارگذاری مجدد، می‌توان دستور reset زیر را صادر کرد:
database.ExecuteSqlRaw("INSERT INTO Chapters_FTS_SpellFix(command) VALUES(\"reset\");");


کوئری گرفتن از جدول مجازی Chapters_FTS_SpellFix

تا اینجا افزونه‌ی spellfix1 را کامپایل و به سیستم معرفی کردیم. سپس جداول واژه نامه‌ی آن‌را نیز تشکیل دادیم، اکنون نوبت به کوئری گرفتن از آن است. به همین جهت یک موجودیت بدون کلید دیگر را بر اساس ساختار خروجی کوئری‌های آن ایجاد کرده:
namespace EFCoreSQLiteFTS.Entities
{
    public class SpellCheck
    {
        public string Word { get; set; }
        public decimal Rank { get; set; }
        public decimal Distance { get; set; }
        public decimal Score { get; set; }
        public decimal Matchlen { get; set; }
    }
}
و آن‌را توسط متد HasNoKey به EF Core معرفی می‌کنیم:
namespace EFCoreSQLiteFTS.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        //...

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

            builder.Entity<SpellCheck>().HasNoKey().ToView(null);
        }

        //...
    }
}
در اینجا SpellCheck تهیه شده با متد HasNoKey علامتگذاری می‌شود تا آن‌را بتوان بدون مشکل در کوئری‌های EF استفاده کرد. همچنین فراخوانی ToView(null) سبب می‌شود تا EF Core جدولی را در حین Migration از روی این موجودیت ایجاد نکند و آن‌را به همین حال رها کند.

در آخر، کوئری گرفتن از این جدول، ساختار زیر را دارد:
foreach (var item in context.Set<SpellCheck>().FromSqlRaw(
          @"SELECT word, rank, distance, score, matchlen FROM Chapters_FTS_SpellFix
            WHERE word MATCH {0} and top=6", "فارشی"))
{
    Console.WriteLine($"Word: {item.Word}");
    Console.WriteLine($"Distance: {item.Distance}");
}
با این خروجی:


top=6 در این کوئری خاص یعنی 6 رکورد را بازگشت بده.

یک نکته: اگر می‌خواهید کوئری فوق را توسط برنامه‌ی «DB Browser for SQLite» اجرا کنید، باید از منوی tools آن، گزینه‌ی load extension را انتخاب کرده و فایل dll افزونه را به برنامه معرفی کنید.


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
مطالب
استفاده از Interop.word برای جایگزین کردن مقادیر در تمامی فایل (Footer - Header - ... )
یکی از متداول‌ترین کارهایی که با اسناد می‌توان انجام داد، تهیه خروجی pdf از word و پر کردن یک فایل word با مقادیر ورودی است که سعی داریم یک نمونه از آن را اینجا بررسی کنیم. کد عمومی برای جایگزین کردن:
public void MsInteropReplace(Microsoft.Office.Interop.Word.Application doc, object findText, object replaceWithText)
        {
            object matchCase = false;
            object matchWholeWord = true;
            object matchWildCards = false;
            object matchSoundsLike = false;
            object matchAllWordForms = false;
            object forward = true;
            object format = false;
            object matchKashida = false;
            object matchDiacritics = false;
            object matchAlefHamza = false;
            object matchControl = false;
            object read_only = false;
            object visible = true;
            object replace = 2;
            object wrap = 1;
            //execute find and replace
            doc.Selection.Find.Execute(ref findText, ref matchCase, ref matchWholeWord,
                ref matchWildCards, ref matchSoundsLike, ref matchAllWordForms, ref forward, ref wrap, ref format, ref replaceWithText, ref replace,
                ref matchKashida, ref matchDiacritics, ref matchAlefHamza, ref matchControl);
        }
 و یا این مورد:
private static void MsInteropReplace2()
        {
            var doc = new Microsoft.Office.Interop.Word.Application().Documents.Open(@"D:\temp\te1.docx");

            doc.Content.Find.Execute("@levelOrder", false, true, false, false, false, true, 1, false, "12345", 2,
            false, false, false, false);
            object missing = System.Reflection.Missing.Value;
            doc.SaveAs(@"D:\temp\out.docx", ref missing, ref missing, ref missing, ref missing
                , ref missing, ref missing, ref missing, ref missing, ref missing, ref missing
                , ref missing, ref missing, ref missing, ref missing, ref missing);
}

که هر دو مورد را در stackoverflow میتوانید پیدا کنید. به شخصه از این مورد برای replace کردن مقادیر در یک فایل template.docx استفاده میکردم؛ ولی بعد از مدتی فهمیدم که Footer‌ها و Header را نمیتواند پردازش کند. کد زیر در تمامی قسمت‌هایی که در یک فایل word می‌توان متغیر تعریف کرد را گشته و عمل پر کردن مقادیر را بر روی فایل نمونه، انجام می‌دهد و شامل سه متد ذیل است:
private static void repAll()
        {
            object Missing = System.Reflection.Missing.Value;

            Application app = null;
            Microsoft.Office.Interop.Word.Document doc = null;
            try
            {
                app = new Microsoft.Office.Interop.Word.Application();

                doc = app.Documents.Open(@"D:\temp\te1.docx", Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing);

                FindReplaceAnywhere(app, "@levelOrder", "محرمانه");

                doc.SaveAs(@"D:\temp\out.docx", Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing, Missing);
            }
            finally
            {
                try
                {
                    if (doc != null) ((Microsoft.Office.Interop.Word._Document)doc).Close(true, Missing, Missing);
                }
                finally { }
                if (app != null) ((Microsoft.Office.Interop.Word._Application)app).Quit(true, Missing, Missing);
            }
        }

        private static void searchAndReplaceInStory(Microsoft.Office.Interop.Word.Range rngStory, string strSearch, string strReplace)
        {
            rngStory.Find.ClearFormatting();
            rngStory.Find.Replacement.ClearFormatting();
            rngStory.Find.Text = strSearch;
            rngStory.Find.Replacement.Text = strReplace;
            rngStory.Find.Wrap = WdFindWrap.wdFindContinue;
            object Missing = System.Reflection.Missing.Value;

            object arg1 = Missing; // Find Pattern
            object arg2 = Missing; //MatchCase
            object arg3 = Missing; //MatchWholeWord
            object arg4 = Missing; //MatchWildcards
            object arg5 = Missing; //MatchSoundsLike
            object arg6 = Missing; //MatchAllWordForms
            object arg7 = Missing; //Forward
            object arg8 = Missing; //Wrap
            object arg9 = Missing; //Format
            object arg10 = Missing; //ReplaceWith
            object arg11 = WdReplace.wdReplaceAll; //Replace
            object arg12 = Missing; //MatchKashida
            object arg13 = Missing; //MatchDiacritics
            object arg14 = Missing; //MatchAlefHamza
            object arg15 = Missing; //MatchControl

            rngStory.Find.Execute(ref arg1, ref arg2, ref arg3, ref arg4, ref arg5, ref arg6, ref arg7, ref arg8, ref arg9, ref arg10, ref arg11, ref arg12, ref arg13, ref arg14, ref arg15);
        }

        // Main routine to find text and replace it,
        //   var app = new Microsoft.Office.Interop.Word.Application();
        public static void FindReplaceAnywhere(Microsoft.Office.Interop.Word.Application app, string findText, string replaceText)
        {
            // http://forums.asp.net/p/1501791/3739871.aspx
            var doc = app.ActiveDocument;

            // Fix the skipped blank Header/Footer problem
            //    http://msdn.microsoft.com/en-us/library/aa211923(office.11).aspx
            Microsoft.Office.Interop.Word.WdStoryType lngJunk = doc.Sections[1].Headers[WdHeaderFooterIndex.wdHeaderFooterPrimary].Range.StoryType;

            // Iterate through all story types in the current document
            foreach (Microsoft.Office.Interop.Word.Range rngStory in doc.StoryRanges)
            {

                // Iterate through all linked stories
                var internalRangeStory = rngStory;

                do
                {
                    searchAndReplaceInStory(internalRangeStory, findText, replaceText);

                    try
                    {
                        //   6 , 7 , 8 , 9 , 10 , 11 -- http://msdn.microsoft.com/en-us/library/aa211923(office.11).aspx
                        switch (internalRangeStory.StoryType)
                        {
                            case Microsoft.Office.Interop.Word.WdStoryType.wdEvenPagesHeaderStory: // 6
                            case Microsoft.Office.Interop.Word.WdStoryType.wdPrimaryHeaderStory:   // 7
                            case Microsoft.Office.Interop.Word.WdStoryType.wdEvenPagesFooterStory: // 8
                            case Microsoft.Office.Interop.Word.WdStoryType.wdPrimaryFooterStory:   // 9
                            case Microsoft.Office.Interop.Word.WdStoryType.wdFirstPageHeaderStory: // 10
                            case Microsoft.Office.Interop.Word.WdStoryType.wdFirstPageFooterStory: // 11

                                if (internalRangeStory.ShapeRange.Count > 0)
                                {
                                    foreach (Microsoft.Office.Interop.Word.Shape oShp in internalRangeStory.ShapeRange)
                                    {
                                        if (oShp.TextFrame.HasText != 0)
                                        {
                                            searchAndReplaceInStory(oShp.TextFrame.TextRange, findText, replaceText);
                                        }
                                    }
                                }
                                break;

                            default:
                                break;
                        }
                    }
                    catch
                    {
                        // On Error Resume Next
                    }

                    // ON ERROR GOTO 0 -- http://www.harding.edu/fmccown/vbnet_csharp_comparison.html

                    // Get next linked story (if any)
                    internalRangeStory = internalRangeStory.NextStoryRange;
                } while (internalRangeStory != null); // http://www.harding.edu/fmccown/vbnet_csharp_comparison.html
            }

        }

برای تهیه pdf نیز می‌توانید به کد زیر مراجعه کنید:
public static void getFileDocxInPdf()
        {
            // Create a new Microsoft Word application object
            Microsoft.Office.Interop.Word.Application word = new Microsoft.Office.Interop.Word.Application();

            // C# doesn't have optional arguments so we'll need a dummy value
            object oMissing = System.Reflection.Missing.Value;

            // Get list of Word files in specified directory
            DirectoryInfo dirInfo = new DirectoryInfo(@"D:\temp");
            FileInfo[] wordFiles = dirInfo.GetFiles("*.docx");

            word.Visible = false;
            word.ScreenUpdating = false;

            foreach (FileInfo wordFile in wordFiles)
            {
                // Cast as Object for word Open method
                Object filename = (Object)wordFile.FullName;

                // Use the dummy value as a placeholder for optional arguments
                Microsoft.Office.Interop.Word.Document doc = word.Documents.Open(ref filename, ref oMissing,
                    ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
                    ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing,
                    ref oMissing, ref oMissing, ref oMissing, ref oMissing);
                doc.Activate();

                object outputFileName = wordFile.FullName.Replace(".docx", ".pdf");
                object fileFormat = WdSaveFormat.wdFormatPDF;

                // Save document into PDF Format
                doc.SaveAs(ref outputFileName,
                    ref fileFormat, ref oMissing, ref oMissing,
                    ref oMissing, ref oMissing, ref oMissing, ref oMissing,
                    ref oMissing, ref oMissing, ref oMissing, ref oMissing,
                    ref oMissing, ref oMissing, ref oMissing, ref oMissing);

                // Close the Word document, but leave the Word application open.
                // doc has to be cast to type _Document so that it will find the
                // correct Close method.                
                object saveChanges = WdSaveOptions.wdDoNotSaveChanges;
                ((_Document)doc).Close(ref saveChanges, ref oMissing, ref oMissing);
                doc = null;
            }

            // word has to be cast to type _Application so that it will find
            // the correct Quit method.
            ((_Application)word).Quit(ref oMissing, ref oMissing, ref oMissing);
            word = null;
        }
مطالب
بهبود کارآیی Reflection در دات نت 7
استفاده‌ی از Reflection در زیر ساخت‌های دات نت و ASP.NET Core، بسیار گسترده‌است؛ به همین جهت هرگونه بهبود کارآیی در این زمینه، نه فقط بر روی خود فریم‌ورک، بلکه تمام برنامه‌هایی که از آن استفاده می‌کنند هم تاثیر گذار است. از این لحاظ دات نت 7 شاهد تغییرات گسترده‌ای است تا حدی که کارآیی برنامه‌های مبتنی بر دات نت 7 ای که از Reflection استفاده می‌کنند، نسبت به نگارش‌های قبلی دات نت، حداقل 2 برابر شده‌است و این برنامه‌ها تنها کاری را که باید انجام دهند، صرفا تغییر target framework مورد استفاده‌ی در آن‌ها به نگارش جدید است. در این مطلب نحوه‌ی رسیدن به این کارآیی بالاتر را بررسی خواهیم کرد.


تدارک یک آزمایش برای بررسی میزان افزایش کارآیی Reflection در دات نت 7

یک برنامه‌ی کنسول جدید را ایجاد کرده و سپس کلاس Person را به صورت زیر به آن اضافه می‌کنیم:
namespace NET7Reflection;

public class Person
{
    private int _age;

    internal Person(int age) => _age = age;

    private int GetAge() => _age;

    private void SetAge(int age) => _age = age;
}
همانطور که مشاهده می‌کنید، سازنده‌ی این کلاس، internal است و همچنین دو متد private هم دارد که اگر بخواهیم از آن در جای  دیگری استفاده کنیم، یکی از روش‌های متداول جهت دسترسی به این امکانات خصوصی، استفاده از Reflection است.
به همین جهت ابتدا کتابخانه‌ی BenchmarkDotNet را با TargetFramework دات نت 7 به صورت زیر به پروژه اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
  </ItemGroup>

</Project>
در ادامه، یک کلاس آزمایش کارآیی برنامه را که با استفاده از Reflection، به امکانات خصوصی کلاس Person دسترسی پیدا می‌کند، مشاهده می‌کنید:
using System.Reflection;
using BenchmarkDotNet.Attributes;

namespace NET7Reflection;

[MemoryDiagnoser(false)]
public class Benchmarks
{
    private readonly object?[] _ageParams = { 30 };

    private readonly ConstructorInfo _ctor =
        typeof(Person).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(int) })!;

    private readonly MethodInfo _getAgeMethod =
        typeof(Person).GetMethod("GetAge", BindingFlags.NonPublic | BindingFlags.Instance)!;

    private readonly Person _person = new(10);

    private readonly MethodInfo _setAgeMethod =
        typeof(Person).GetMethod("SetAge", BindingFlags.NonPublic | BindingFlags.Instance)!;

    [Benchmark]
    public int GetAge() =>
        (int)typeof(Person).GetMethod("GetAge", BindingFlags.NonPublic | BindingFlags.Instance)!
                           .Invoke(_person, Array.Empty<object?>())!;

    [Benchmark]
    public int GetAgeCachedMethod() => (int)_getAgeMethod.Invoke(_person, Array.Empty<object?>())!;

    [Benchmark]
    public void SetAge() =>
        typeof(Person).GetMethod("SetAge", BindingFlags.NonPublic | BindingFlags.Instance)!
                      .Invoke(_person, new object?[] { 30 });

    [Benchmark]
    public void SetAgeCachedMethod() => _setAgeMethod.Invoke(_person, new object?[] { 30 });

    [Benchmark]
    public void SetAgeCachedMethodCachedParams() => _setAgeMethod.Invoke(_person, _ageParams);

    [Benchmark]
    public Person Ctor() =>
        (Person)typeof(Person).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new[] { typeof(int) })!
                              .Invoke(_person, new object?[] { 30 })!;

    [Benchmark]
    public Person CtorCachedCtorInfo() => (Person)_ctor.Invoke(_person, new object?[] { 30 })!;

    [Benchmark]
    public Person CtorCachedCtorInfoCachedParams() => (Person)_ctor.Invoke(_person, _ageParams)!;
}
توضیحات:
- در اینجا نحوه‌ی کار با متدهای خصوصی کلاس Person را توسط Reflection مشاهده می‌کنید. برای مثال در متد GetAge، به نحو متداولی این کار صورت گرفته‌است. در متد GetAgeCachedMethod، قسمت دریافت اطلاعات متد، کش شده‌است و برای نمونه در متد SetAgeCachedMethodCachedParams، هم کش شدن قسمت دریافت اطلاعات متد را مشاهده می‌کنید و هم کش شدن پارامتر ارسالی به آن‌را.
- این آزمایش را با فراخوانی زیر و تنظیم target framework به دات نت 6 و سپس دات نت 7، به صورت جداگانه‌ای اجرا می‌کنیم:
using BenchmarkDotNet.Running;
using NET7Reflection;

BenchmarkRunner.Run<Benchmarks>();
حاصل اجرای آن با target framework دات نت 6 به صورت زیر است:



و با target framework دات نت 7 به صورت زیر:


همانطور که مشاهده می‌کنید، در اکثر موارد، کارآیی Reflection در دات نت 7، حداقل 2 برابر نمونه‌ی مشابه دات نت 6 است.


چه تغییری در دات نت 7 سبب بهبود قابل ملاحظه‌ی کارآیی Reflection شده‌است؟

جزئیات تغییرات صورت گرفته‌ی در Reflection دات نت 7 را می‌توانید در این pull request مشاهده کنید که در حقیقت از امکانات سطح پایین IL Emit استفاده کرده‌اند. در این مورد پیشتر تعدادی مطلب ذیل عنوان «آشنایی با Reflection.Emit» در این سایت منتشر شده‌اند که می‌توانید آن‌ها را بررسی کنید.
در کل هرچند تغییرات جدید دات نت مانند ارائه‌ی انواع و اقسام source generators، در تعدادی از موارد نیاز به Reflection را کمتر کرده‌اند و کارآیی بیشتری را ارائه داده‌اند، اما Reflection هیچگاه منسوخ نخواهد شد و هرگونه بهبود کارآیی در این زمینه، بر روی کل فریم‌ورک و برنامه‌های مشتق شده‌ی از آن، تاثیر قابل توجهی را خواهد گذاشت.
مطالب
نگاهی به درون سیستم Binding در WPF و یافتن مواردی که هنوز در حافظه‌اند
در WPF، زیر ساخت‌های ComponentModel توسط کلاسی به نام PropertyDescriptor، منابع Binding موجود در قسمت‌های مختلف برنامه را در جدولی عمومی ذخیره و نگهداری می‌کند. هدف از آن، مطلع بودن از مواردی است که نیاز دارند توسط مکانیزم‌هایی مانند INotifyPropertyChanged و DependencyProperty ها، اطلاعات اشیاء متصل را به روز کنند.
در این سیستم، کلیه اتصالاتی که Mode آن‌ها به OneTime تنظیم نشده است، به صورت اجباری دارای یک valueChangedHandlers متصل توسط سیستم PropertyDescriptor خواهند بود و در حافظه زنده نگه داشته می‌شوند؛ تا بتوان در صورت نیاز، توسط سیستم binding اطلاعات آن‌ها را به روز کرد.
همین مساله سبب می‌شود تا اگر قرار نیست خاصیتی برای نمونه توسط مکانیزم INotifyPropertyChanged اطلاعات UI را به روز کند (یک خاصیت معمولی دات نتی است) و همچنین حالت اتصال آن به OneTime نیز تنظیم نشده، سبب مصرف حافظه بیش از حد برنامه شود.
اطلاعات بیشتر
A memory leak may occur when you use data binding in Windows Presentation Foundation

راه حل آن هم ساده است. برای اینکه valueChangedHandler ایی به خاصیت ساده‌ای که قرار نیست بعدها UI را به روز کند، متصل نشود، حالت اتصال آن‌را باید به OneTime تنظیم کرد.


سؤال: در یک برنامه بزرگ که هم اکنون مشغول به کار است، چطور می‌توان این مسایل را ردیابی کرد؟

برای دستیابی به اطلاعات کش Binding در WPF، باید به Reflection متوسل شد. به این ترتیب در برنامه جاری، در کلاس PropertyDescriptor به دنبال یک کلاس خصوصی تو در توی دیگری به نام ReflectTypeDescriptionProvider خواهیم گشت (این اطلاعات از طریق مراجعه به سورس دات نت و یا حتی برنامه‌های ILSpy و Reflector قابل استخراج است) و سپس در این کلاس خصوصی داخلی، فیلد خصوصی propertyCache آن‌را که از نوع  HashTable است استخراج می‌کنیم:
 var reflectTypeDescriptionProvider = typeof(PropertyDescriptor).Module.GetType("System.ComponentModel.ReflectTypeDescriptionProvider");
var propertyCacheField = reflectTypeDescriptionProvider.GetField("_propertyCache",
BindingFlags.Static | BindingFlags.NonPublic);


اکنون به لیست داخلی Binding نگهداری شونده توسط WPF دسترسی پیدا کرده‌ایم. در این لیست به دنبال مواردی خواهیم گشت که فیلد valueChangedHandlers به آن‌ها متصل شده است  و در حال گوش فرا دادن به سیستم binding هستند (سورس کامل و طولانی این مبحث را در پروژه پیوست شده می‌توانید ملاحظه کنید).


یک مثال: تعریف یک کلاس ساده، اتصال آن و سپس بررسی اطلاعات درونی سیستم Binding

فرض کنید یک کلاس مدل ساده به نحو ذیل تعریف شده است:
namespace WpfOneTime.Models
{
    public class User
    {
        public string Name { set; get; }
    }
}
سپس این کلاس به صورت یک List، توسط ViewModel برنامه در اختیار View متناظر با آن قرار می‌گیرد:
using WpfOneTime.Models;
using System.Collections.Generic;

namespace WpfOneTime.ViewModels
{
    public class MainWindowViewModel
    {
        public IList<User> Users { set; get; }

        public MainWindowViewModel()
        {
            Users = new List<User>();
            for (int i = 0; i < 1000; i++)
            {
                Users.Add(new User { Name = "name " + i });
            }
        }
    }
}
تعاریف View برنامه نیز به نحو زیر است:
<Window x:Class="WpfOneTime.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ViewModels="clr-namespace:WpfOneTime.ViewModels"        
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ViewModels:MainWindowViewModel x:Key="vmMainWindowViewModel" />
    </Window.Resources>
    <Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">        
        <ListBox ItemsSource="{Binding Users}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>
همه چیز در آن معمولی به نظر می‌رسد. ابتدا به ViewModel برنامه دسترسی یافته و  DataContext را با آن مقدار دهی می‌کنیم. سپس اطلاعات این لیست را توسط یک ListBox نمایش خواهیم داد.
خوب؛ اکنون اگر اطلاعات HashTable داخلی سیستم Binding را در مورد View فوق بررسی کنیم به شکل زیر خواهیم رسید:


بله. تعداد زیادی خاصیت Name زنده و موجود در حافظه باقی هستند که تحت ردیابی سیستم Binding می‌باشند.
در ادامه، نکته‌ی ابتدای بحث را جهت تعیین حالت Binding به OneTime، به View فوق اعمال می‌کنیم (یک سطر ذیل باید تغییر کند):
 <TextBlock Text="{Binding Name, Mode=OneTime}" />
در این حالت اگر نگاهی به سیستم ردیابی WPF داشته باشیم، دیگر خبری از اشیاء زنده دارای خاصیت Name در حال ردیابی نیست:


به این ترتیب می‌توان در لیست‌های طولانی، به مصرف حافظه کمتری در برنامه WPF خود رسید.
بدیهی است این نکته را تنها در مواردی می‌توان اعمال کرد که نیاز به به‌روز رسانی‌های ثانویه اطلاعات UI در کدهای برنامه وجود ندارند.


چطور از این نکته برای پروفایل یک برنامه موجود استفاده کنیم؟

کدهای برنامه را از انتهای بحث دریافت کنید. سپس دو فایل ReflectPropertyDescriptorWindow.xaml و ReflectPropertyDescriptorWindow.xaml.cs آن‌را به پروژه خود اضافه نمائید و در سازنده پنجره اصلی برنامه، کد ذیل را فراخوانی نمائید:
 new ReflectPropertyDescriptorWindow().Show();
کمی با برنامه کار کرده و منتظر شوید تا لیست نهایی اطلاعات داخلی Binding ظاهر شود. سپس مواردی را که دارای HandlerCount بالا هستند، مدنظر قرار داده و بررسی نمائید که آیا واقعا این اشیاء نیاز به valueChangedHandler متصل دارند یا خیر؟ آیا قرار است بعدها UI را از طریق تغییر مقدار خاصیت آن‌ها به روز نمائیم یا خیر. اگر خیر، تنها کافی است نکته Mode=OneTime را به این Bindingها اعمال نمائیم.

دریافت کدهای کامل پروژه این مطلب
WpfOneTime.zip
مطالب
طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD

هدف، ارائه راه‌حلی برای نمایش جدولی اطلاعات، جستجو، مرتب سازی و صفحه بندی و همچنین انجام عملیات ثبت، ویرایش و حذف بر روی آنها به صورت Ajaxای در بخش back office نرم افزار می‌باشد.

پیش نیازها:

ایده کار به این شکل می‌باشد که برای نمایش اطلاعات به صورت جدولی با قابلیت‌های مذکور، لازم است یک اکشن Index برای نمایش اولیه و صفحه اول اطلاعات صفحه بندی شده و اکشن متدی به نام List برای پاسخ به درخواست‌های صفحه بندی، مرتب سازی، تغییر تعداد آیتم‌ها در هر صفحه و همچنین جستجو، داشته باشیم که این اکشن متد List، بعد از واکشی اطلاعات مورد نظر از منبع داده، آنها را به همراه اطلاعاتی که در کوئری استرینگ درخواست جاری وجود دارد در قالب یک PartialView به کلاینت ارسال کند.


ایجاد مدل‌های پایه

همانطور که در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» مطرح شد، برای پیاده سازی متدهای GetPagedList در ApplicationService‌ها از الگوی Request/Response استفاده می‌کنیم. برای این منظور واسط و کلاس‌های زیر را خواهیم داشت:

واسط IPagedQueryModel

    public interface IPagedQueryModel
    {
        int Page { get; set; }
        int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        string SortExpression { get; set; }
    }

این واسط قراردادی می‌باشد برای نوع و نام پارامترهایی که توسط کلاینت به سرور ارسال می‌شود. پراپرتی SortExpression آن، نام و ترتیب مرتب سازی را مشخص می‌کند؛ برای این منظور FieldName_ASC و FieldName_DESC به ترتیب برای حالات مرتب سازی صعودی و نزولی براساس FieldName مقدار دهی خواهد شد.

برای جلوگیری از تکرار این خصوصیات در مدل‌های کوئری مربوط به موجودیت‌ها، میتوان کلاس پایه‌ای به شکل زیر در نظر گرفت که پیاده ساز واسط بالا می‌باشد:

  public class PagedQueryModel : IPagedQueryModel, IShouldNormalize
    {
        public int Page { get; set; }
        public int PageSize { get; set; }

        /// <summary>
        ///     Expression of Sorting.
        /// </summary>
        /// <example>
        ///     Examples:
        ///     "Name_ASC"
        /// </example>
        public string SortExpression { get; set; }

        public virtual void Normalize()
        {
            if (Page < 1)
                Page = 1;

            if (PageSize < 1)
                PageSize = 10;

            if (SortExpression.IsEmpty())
                SortExpression = "Id_DESC";
        }
    }

مدل بالا علاوه بر پیاده سازی واسط IPagedQueryModel، پیاده ساز واسط IShouldNormalize نیز می‌باشد؛ دلیل وجود چنین واسطی در مقاله «طراحی و پیاده سازی ServiceLayer به همراه خودکارسازی Business Validationها» توضیح داده شده است:

پیاده سازی واسط IShouldNormalize باعث خواهد شد که قبل از اجرای خود متد، این نوع پارامترها با استفاده از یک Interceptor شناسایی شده و متد Normalize آنها اجرا شود.

کلاس PagedQueryResult

    public class PagedQueryResult<TModel>
    {
        public PagedQueryResult()
        {
            Items = new List<TModel>();
        }
        public IEnumerable<TModel> Items { get; set; }
        public long TotalCount { get; set; }
    }

دلیل وجود کلاس بالا در مقاله «طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور» توضیح داده شده است:

عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.

کلاس PagedListModel

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

    public class PagedListModel<TModel>
    {
        public IPagedQueryModel Query { get; set; }

        public PagedQueryResult<TModel> Result { get; set; }
    }

پراپرتی Query در برگیرنده پارامتر ورودی اکشن متد List می‌باشد که پراپرتی‌های آن با مقادیر موجود در کوئری استرینگ درخواست جاری مقدار دهی شده‌اند؛ البته بدون وجود کلاس بالا نیز به کمک ViewBag می‌شود این اطلاعات ترکیبی را به ویو ارسال کرد که پیشنهاد نمی‌شود.


متد GetPagedListAsync موجود در CrudApplicationService

    public abstract class CrudApplicationService<TEntity, TModel, TCreateModel, TEditModel, TDeleteModel,
        TPagedQueryModel, TDynamicQueryModel> : ApplicationService,
        ICrudApplicationService<TModel, TCreateModel, TEditModel, TDeleteModel, TPagedQueryModel, TDynamicQueryModel>
        where TEntity : Entity, new()
        where TCreateModel : class
        where TEditModel : class, IModel
        where TModel : class, IModel
        where TDeleteModel : class, IModel
        where TPagedQueryModel : PagedQueryModel, new()
        where TDynamicQueryModel : DynamicQueryModel

    {

        #region Properties

        protected IQueryable<TEntity> UnTrackedEntitySet => EntitySet.AsNoTracking();
        public IUnitOfWork UnitOfWork { get; set; }
        public IMapper Mapper { get; set; }
        protected IDbSet<TEntity> EntitySet => UnitOfWork.Set<TEntity>();

        #endregion

        #region ICrudApplicationService Members

        #region Methods

        public virtual async Task<PagedQueryResult<TModel>> GetPagedListAsync(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            var query = ApplyFiltering(model);

            var totalCount = await query.LongCountAsync().ConfigureAwait(false);

            var result = query.ProjectTo<TModel>(Mapper.ConfigurationProvider);

            result = result.ApplySorting(model);
            result = result.ApplyPaging(model);

            return new PagedQueryResult<TModel>
            {
                Items = await result.ToListAsync().ConfigureAwait(false),
                TotalCount = totalCount
            };
        }
        #endregion

        #endregion

        #region Protected Methods

        /// <summary>
        ///     Apply Filtering To GetPagedList and GetPagedListAsync
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        protected virtual IQueryable<TEntity> ApplyFiltering(TPagedQueryModel model)
        {
            Guard.ArgumentNotNull(model, nameof(model));

            return UnTrackedEntitySet;
        }
        #endregion
    }

در بدنه این متد، ابتدا عملیات جستجو توسط متد ApplyFiltering انجام می‌شود. این متد به صورت پیش فرض هیچ شرطی را بر روی کوئری ارسالی به منبع داده اعمال نمی‌کند؛ مگر اینکه توسط زیر کلاس‌ها بازنویسی شود و فیلترهای مورد نیاز اعمال شوند. سپس تعداد کل آیتم‌های فیلتر شده محاسبه شده و بعد از عملیات Projection، مرتب سازی و صفحه بندی انجام می‌گیرد. برای مباحث مرتب سازی و صفحه بندی از دو متد زیر کمک گرفته شده‌است:

    public static class QueryableExtensions
    {
        public static IQueryable<TModel> ApplySorting<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return query.OrderBy(request.SortExpression.Replace('_', ' '));
        }

        public static IQueryable<TModel> ApplyPaging<TModel>(this IQueryable<TModel> query, IPagedQueryModel request)
        {
            Guard.ArgumentNotNull(request, nameof(request));
            Guard.ArgumentNotNull(query, nameof(query));

            return request != null
                ? query.Page((request.Page - 1) * request.PageSize, request.PageSize)
                : query;
        }
    }

به منظور مرتب سازی از کتابخانه  System.Liq.Dynamic کمک گرفته شده‌است.

نکته: مشخص است که این روش، وابستگی به وجود متد GetPagedListAsync ندارد و صرفا برای تشریح ارتباط مطالبی که قبلا منتشر شده بود، مطرح شد.


پیاده سازی اکشن متدهای Index و List

public partial class RolesController : BaseController
{
    #region Fields
        private readonly IRoleService _service;
        private readonly ILookupService _lookupService;

        #endregion

    #region Constractor
        public RolesController(IRoleService service,  ILookupService lookupService)
        {
            Guard.ArgumentNotNull(service, nameof(service));
            Guard.ArgumentNotNull(lookupService, nameof(lookupService));

            _service = service;
            _lookupService = lookupService;
        }
        #endregion

    #region Index / List
    [HttpGet]
    public virtual async Task<ActionResult> Index()
    {
        var query = new RolePagedQueryModel();
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var pagedList = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        var model = new RoleIndexViewModel
        {
            PagedListModel = pagedList,
            Permissions = _lookupService.GetPermissions()
        };
        return View(model);
    }
    [HttpGet, AjaxOnly, NoOutputCache]
    public virtual async Task<ActionResult> List(RolePagedQueryModel query)
    {
        var result = await _service.GetPagedListAsync(query).ConfigureAwait(false);

        var model = new PagedListModel<RoleModel>
        {
            Query = query,
            Result = result
        };

        return PartialView(MVC.Administration.Roles.Views._List, model);
    }
    #endregion
}

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

    public class RolePagedQueryModel : PagedQueryModel
    {
        public string Name { get; set; }
        public string Permission { get; set; }
    }

در این مورد خاص لازم است لیست دسترسی‌های موجود درسیستم به صورت لیستی برای انتخاب در فرم جستجو مهیا باشد. فرم جستجو در ویو مربوط به اکشن Index قرار می‌گیرد و قرار نیست به همراه پارشال ویو List_ در هر درخواستی از سرور دریافت شود. لذا لازم است مدلی برای ویو Index در نظر بگیریم که به شکل زیر می‌باشد:

    public class RoleIndexViewModel
    {   
        public RoleIndexViewModel()
        {
            Permissions = new List<LookupItem>();
        }
        public IReadOnlyList<LookupItem> Permissions { get; set; }
        public PagedListModel<RoleModel> PagedListModel { get; set; }
    }

پراپرتی PagedListModel در برگیرنده اطلاعات مربوط به نمایش اولیه جدول اطلاعات می‌باشد و پراپرتی Permissions لیست دسترسی‌های موجود درسیستم را به ویو منتقل خواهد کرد. اگر ویو ایندکس شما به داده اضافه ای نیاز ندارد، از ایجاد مدل بالا صرف نظر کنید.


ویو Index.cshtml

@model RoleIndexViewModel

@{
    ViewBag.Title = L("Administration.Views.Role.Index.Title");
    ViewBag.ActiveMenu = AdministrationMenuNames.RoleManagement;
}

<div class="row">
    <div class="col-md-12">
        <div id="filterPanel" class="panel-collapse collapse" role="tabpanel" aria-labelledby="filterPanel">
            <div class="panel panel-default margin-bottom-5">

                <div class="panel-body">
                    @using (Ajax.BeginForm(MVC.Administration.Roles.List(),
new AjaxOptions { UpdateTargetId = "RolesList", HttpMethod = "GET" }, new { id = "filterForm", data_submit_on_reset = "true" }))
                    {
                        <div class="row">
                            <div class="col-md-3">
                                <input type="text" name="Name" class="form-control" value="" placeholder="@L("Administration.Role.Fields.Name")" />
                            </div>
                            <div class="col-md-3">
                                @Html.DropDownList("Permission", Model.Permissions.ToSelectListItems(), L("Administration.Views.Role.FilterBy.Permission"),new {@class="form-control"})
                            </div>
                            <div class="col-md-3">

                                <button type="submit"
                                        role="button"
                                        class="btn btn-info">
                                    @L("Commands.Filter")
                                </button>
                                <button type="reset"
                                        role="button"
                                        class="btn btn-default">
                                    <i class="fa fa-close"></i>
                                    @L("Commands.Reset")
                                </button>
                            </div>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

<div class="row">
    <div class="col-md-12" id="RolesList">
        @{Html.RenderPartial(MVC.Administration.Roles.Views._List, Model.PagedListModel);}
    </div>
</div>

فرم جستجو باید دارای ویژگی data_submit_on_reset با مقدار "true" باشد. به منظور پاکسازی فرم جستجو و ارسال درخواست جستجو با فرمی خالی از داده، برای بازگشت به حالت اولیه از تکه کد زیر استفاده خواهد شد:

  $(document).on("reset", "form[data-submit-on-reset]",
            function () {
                var form = this;
                setTimeout(function () {
                    $(form).submit();
                });
            });

در ادامه پارشال ویو List_ با داده ارسالی به ویو Index، رندر شده و کار نمایش اولیه اطلاعات به صورت جدولی به اتمام می‌رسد.


پارشال ویو List.cshtml_

@model PagedListModel<RoleModel>
@{
    Layout = null;
    var rowNumber = (Model.Query.Page - 1) * Model.Query.PageSize + 1;
    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));
}

<div class="panel panel-default margin-bottom-5">
    <table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">
        <thead>
            <tr>
                <th style="width: 5%;">
                    #
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("DisplayName", L("Administration.Role.Fields.DisplayName"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
                <th class="col-md-3 sortable">
                    @Html.SortableColumn("IsDefault", L("Administration.Role.Fields.IsDefault"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
                </th>
               
                <th style="width: 5%;"></th>
            </tr>
        </thead>

        <tbody>
           @foreach (var role in Model.Result.Items)
            {
                <tr>
                    <td>@(rowNumber++.ToPersianNumbers())</td>
                    <td>@role.Name</td>
                    <td>@role.DisplayName</td>
                    <td class="text-center">@Html.DisplayFor(a => role.IsDefault)</td>
                    <td class="text-center operations">
                      
                        <div class="btn-group">

                            <span class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></span>
                            <ul class="dropdown-menu dropdown-menu-left">
                                <li>
                                    <a href="#"
                                       role="button"
                                       data-ajax="true"
                                       data-ajax-method="GET"
                                       data-ajax-update="#main-modal div.modal-content"
                                       data-ajax-url="@Url.Action(MVC.Administration.Roles.Edit(role.Id))"
                                       data-toggle="modal"
                                       data-target="#main-modal">
                                        <i class="fa fa-pencil"></i>
                                        @L("Commands.Edit")
                                    </a>
                                </li>
                                <li>
                                    <a href="#"
                                       role="button"
                                       id="delete-@role.Id"
                                       data-delete-url="@Url.Action(MVC.Administration.Roles.Delete())"
                                       data-delete-model='{"Id":"@role.Id","RowVersion":"@Convert.ToBase64String(role.RowVersion)"}'>
                                        <i class="fa fa-trash"></i>
                                        @L("Commands.Delete")
                                    </a>
                                </li>
                            </ul>
                        </div>
                    </td>
                </tr>
            }
        </tbody>
    </table>

</div>

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>

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

    var refreshUrl = Url.Action(MVC.Administration.Roles.List().AddRouteValues(new RouteValueDictionary(Model.Query.ToDictionary())));

refreshUrl برای ارسال درخواست به اکشن متد List در نظر گرفته شده‌است که در کوئری استرینگ مربوط به خود، اطلاعاتی (مرتب سازی، شماره صفحه، اطلاعات جستجو و همچنین تعداد آیتم‌های موجود در هر صفحه) را دارد که حالت فعلی گرید را می‌توانیم دوباره از سرور درخواست کنیم.

<table class="table table-bordered table-hover" id="RolesListTable" data-ajax-refresh-url="@refreshUrl" data-ajax-refresh-update="#RolesList">

دو ویژگی data-ajax-refresh-url و data-ajax-refresh-update برای جدولی که لازم است عملیات CRUD را پشتیبانی کند، لازم می‌باشد. در قسمت دوم به استفاده از این دو ویژگی در هنگام عملیات ثبت، ویرایش و حذف خواهیم پرداخت.

<th class="col-md-3 sortable">
    @Html.SortableColumn("Name", L("Administration.Role.Fields.Name"), Model.Query, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
</th>

ستونی که امکان مرتب سازی را دارد باید th آن، کلاس sortable را داشته باشد. همچنین باید از هلپری که پیاده سازی آن را در ادامه خواهیم دید، استفاده کنیم. این هلپر، نام فیلد، عنوان ستون، مدل Query و همچین یک urlFactory را در قالب یک ‎Func<RouteValueDictionary,string>‎ دریافت می‌کند.


پیاده سازی هلپر SortableColumn

        public static MvcHtmlString SortableColumn(this HtmlHelper html, string columnName,
            string columnDisplayName, IPagedQueryModel queryModel, string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
        {
            var dictionary = queryModel.ToDictionary();

            var routeValueDictionary = new RouteValueDictionary(dictionary)
            {
                ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
                    ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
                        ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                            ? string.Empty : $"{columnName}_DESC"
            };

            var url = urlFactory(routeValueDictionary);

            var aTag = new TagBuilder("a");
            aTag.Attributes.Add("href", "#");
            aTag.Attributes.Add("data-ajax", "true");
            aTag.Attributes.Add("data-ajax-method", "GET");
            aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
            aTag.Attributes.Add("data-ajax-url", url);
            aTag.InnerHtml = columnDisplayName;

            var iconCssClass = !queryModel.SortExpression.StartsWith(columnName)
                ? "fa-sort"
                : queryModel.SortExpression.EndsWith("DESC")
                    ? "fa-sort-down"
                    : "fa-sort-up";

            var iTag = new TagBuilder("i");
            iTag.AddCssClass($"fa {iconCssClass}");

            return new MvcHtmlString($"{aTag}\n{iTag}");
        }

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

public static IDictionary<string, object> ToDictionary(this object source)
{
    return source.ToDictionary<object>();
}

public static IDictionary<string, T> ToDictionary<T>(this object source)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    var dictionary = new Dictionary<string, T>();

    foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(source))
    {
        AddPropertyToDictionary(property, source, dictionary);
    }
    return dictionary;
}

private static void AddPropertyToDictionary<T>(PropertyDescriptor property, object source,
    IDictionary<string, T> dictionary)
{
    var value = property.GetValue(source);

    var items = value as IEnumerable;

    if (items != null && !(items is string))
    {
        var i = 0;
        foreach (var item in items)
        {
            dictionary.Add($"{property.Name}[{i++}]", (T)item);
        }
    }
    else if (value is T)
    {
        dictionary.Add(property.Name, (T)value);
    }

}

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

در ادامه پیاده سازی هلپر SortableColumn، از دیکشنری حاصل، یک وهله از RouteValueDictionary ساخته می‌شود. در زمان رندر شدن PartialView لازم است مشخص شود که برای دفعه بعدی که بر روی این ستون کلیک می‌شود، باید چه مقداری با پارامتر SortExpression موجود در کوئری استرینگ ارسال شود. از این جهت برای پشتیبانی ستون، از حالت‌های مرتب سازی صعودی، نزولی و برگشت به حالت اولیه بدون مرتب سازی، کد زیر را خواهیم داشت:

var routeValueDictionary = new RouteValueDictionary(dictionary)
{
    ["SortExpression"] = !queryModel.SortExpression.StartsWith(columnName)
        ? $"{columnName}_DESC" : queryModel.SortExpression.EndsWith("DESC")
            ? $"{columnName}_ASC" : queryModel.SortExpression.EndsWith("ASC")
                ? string.Empty : $"{columnName}_DESC"
};

در ادامه urlFactory با routeValueDictionary حاصل، Invoke می‌شود تا url نهایی برای مرتب سازی‌های بعدی را  از طریق یک لینک تزئین شده با data اتریبیوت‌های Unobtrusive Ajax در th مربوطه قرار دهیم.

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

<div class="row">
    <div class="col-md-8">
        @Html.Pager(Model, "RolesList", routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)))
        @Html.PageSize("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), new { @class = "margin-right-5" }, "filterForm")
        @Html.Refresh("RolesList", Model.Query, routeValueDictionary => Url.Action(MVC.Administration.Roles.List().AddRouteValues(routeValueDictionary)), L("Commands.Refresh"))
    </div>
</div>


پیاده سازی هلپر Pager

public static MvcHtmlString Pager<TModel>(this HtmlHelper html, PagedListModel<TModel> model,
        string updateTargetId, Func<RouteValueDictionary, string> urlFactory)
{
    return html.PagedListPager(
        new StaticPagedList<TModel>(model.Result.Items, model.Query.Page, model.Query.PageSize,
            (int)model.Result.TotalCount), page =>
       {
           var dictionary = model.Query.ToDictionary();
           var routeValueDictionary = new RouteValueDictionary(dictionary) { ["Page"] = page };
           return urlFactory(routeValueDictionary);
       }, PagedListRenderOptions.EnableUnobtrusiveAjaxReplacing(
            new PagedListRenderOptions
            {
                DisplayLinkToFirstPage = PagedListDisplayMode.Always,
                DisplayLinkToLastPage = PagedListDisplayMode.Always,
                DisplayLinkToPreviousPage = PagedListDisplayMode.Always,
                DisplayLinkToNextPage = PagedListDisplayMode.Always,
                MaximumPageNumbersToDisplay = 6,
                DisplayItemSliceAndTotal = true,
                DisplayEllipsesWhenNotShowingAllPageNumbers = true,
                ItemSliceAndTotalFormat = $"تعداد کل: {model.Result.TotalCount.ToPersianNumbers()}",
                FunctionToDisplayEachPageNumber = page => page.ToPersianNumbers(),
            },
            new AjaxOptions
            {
                AllowCache = false,
                HttpMethod = "GET",
                InsertionMode = InsertionMode.Replace,
                UpdateTargetId = updateTargetId
            }));
}

در متد بالا از کتابخانه PagedList.Mvc استفاده شده‌است. یکی از overload‌های متد PagedListPager آن، یک پارامتر از نوع Func<int, string>‎ به نام generatePageUrl را دریافت می‌کند که امکان شخصی سازی فرآیند تولید لینک به صفحات بعدی و قبلی را به ما می‌دهد. ما نیز از این امکان برای افزودن اطلاعات موجود در مدل Query، به کوئری استرینگ لینک‌های تولیدی استفاده کردیم و صرفا برای لینک‌های ایجادی لازم بود مقادیر پارامتر Page موجود در کوئری استرینگ تغییر کند که در کد بالا مشخص می‌باشد.


پیاده سازی هلپر PageSize

public static MvcHtmlString PageSize(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel, Func<RouteValueDictionary, string> urlFactory, object htmlAttributes = null, string filterFormId = null, params int[] numbers)
{
    if (numbers.Length == 0)
        numbers = new[] { 10, 20, 30, 50, 100 };

    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary)
    {
        [nameof(IPagedQueryModel.Page)] = 1
    };
    routeValueDictionary.Remove(nameof(IPagedQueryModel.PageSize));

    var url = urlFactory(routeValueDictionary);

    var formTag = new TagBuilder("form");
    formTag.Attributes.Add("action", url);
    formTag.Attributes.Add("method", "GET");
    formTag.Attributes.Add("data-ajax", "true");
    formTag.Attributes.Add("data-ajax-method", "GET");
    formTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    formTag.Attributes.Add("data-ajax-url", url);

    if (htmlAttributes != null)
        formTag.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));

    formTag.AddCssClass("form-inline inline");

    var items = numbers.Select(number =>
        new SelectListItem
        {
            Value = number.ToString(),
            Text = number.ToString().ToPersianNumbers(),
            Selected = queryModel.PageSize == number
        });

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

    if (filterFormId.IsEmpty()) return new MvcHtmlString($"{formTag}");

    // ReSharper disable once MustUseReturnValue
    var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";

    return new MvcHtmlString($"{formTag}\n{scriptBlock}");
}

ایده کار به این صورت است که یک المنت select، درون یک المنت form قرار می‌گیرد و در زمان change آن، فرم مربوطه submit می‌شود.

    formTag.InnerHtml = html.DropDownList(nameof(IPagedQueryModel.PageSize), items, new { @class = "form-control page-size", onchange = "$(this.form).submit();" }).ToString();

در زمان تغییر تعداد نمایشی آیتم‌ها در هر صفحه، لازم است حالت فعلی گرید حفظ شود و صرفا پارامتر Page ریست شود.


نکته مهم: در این طراحی اگر فرم جستجویی دارید، در زمان جستجو هیچیک از پارامتر‌های مربوط به صفحه بندی و مرتب سازی به سرور ارسال نخواهند شد (در واقع ریست می‌شوند) و کافیست یک درخواست GET معمولی با ارسال محتویات فرم به سرور صورت گیرد؛ ولی لازم است PageSize تنظیم شده، در زمان اعمال فیلتر نیز به سرور ارسال شود. از این جهت اسکریپتی برای ایجاد یک input مخفی در فرم جستجو نیز هنگام رندر شدن PartialView در صفحه تزریق می‌شود.

  var scriptBlock = $"<script type=\"text/javascript\"> if(window.jQuery){{$('form#{filterFormId}').find('input[name=\"{nameof(IPagedQueryModel.PageSize)}\"]').remove();\n $('form#{filterFormId}').append(\"<input type='hidden' name='{nameof(IPagedQueryModel.PageSize)}' value='{queryModel.PageSize}'/>\")}}</script>";


پیاده سازی هلپر Refresh

public static MvcHtmlString Refresh(this HtmlHelper html, string updateTargetId, IPagedQueryModel queryModel,
    Func<RouteValueDictionary, string> urlFactory, string label = null)
{
    var dictionary = queryModel.ToDictionary();

    var routeValueDictionary = new RouteValueDictionary(dictionary);

    var url = urlFactory(routeValueDictionary);

    var aTag = new TagBuilder("a");
    aTag.Attributes.Add("href", "#");
    aTag.Attributes.Add("role", "button");
    aTag.Attributes.Add("data-ajax", "true");
    aTag.Attributes.Add("data-ajax-method", "GET");
    aTag.Attributes.Add("data-ajax-update", updateTargetId.StartsWith("#") ? updateTargetId : $"#{updateTargetId}");
    aTag.Attributes.Add("data-ajax-url", url);
    aTag.AddCssClass("btn btn-default");

    var iTag = new TagBuilder("i");
    iTag.AddCssClass("fa fa-refresh");

    aTag.InnerHtml = $"{iTag} {label}";

    return new MvcHtmlString(aTag.ToString());
}

متد بالا نیز به مانند refreshUrl که پیشتر مطرح شد، برای بارگزاری مجدد حالت فعلی گرید استفاده می‌شود و از این جهت است که مقادیر مربوط به کلیدهای routeValueDictionary  را تغییر نداده‌ایم.


روش دیگر برای مدیریت این چنین کارهایی، استفاده از یک المنت form و قرادادن کل گرید به همراه یک سری input مخفی معادل با پارامترهای دریافتی اکشن متد List و مقدار دهی آنها در زمان کلیک بر روی دکمه‌های صفحه بندی، بارگزاری مجدد، دکمه اعمال فیلتر و لیست آبشاری تنظیم تعداد آیتم‌ها، درون آن نیز میتواند کار ساز باشد؛ اما در زمان پیاده سازی خواهید دید که پیاده سازی آن خیلی سرراست، به مانند پیاده سازی موجود در مطلب جاری نخواهد بود. 

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

مطالب
استفاده از ماژول Remote
همانطور که در مقاله «آغاز کار با الکترون» گفتیم، فرآیند اصلی، تنها فرآیندی است که توانایی استفاده از گرافیک بومی سیستم عامل را دارد. ولی بسیاری از اوقات نیاز است در سمت renderProcess توانایی انجام این کار‌ها را داشته باشیم. در این مقاله قصد داریم که همان دیالوگ‌های open و save را از طریق Render Process اجرا نماییم.
الکترون برای اینکار از یک ماژول به نام remote استفاده می‌کند که وظیفه آن برقراری ارتباط IPC از Render Process به Main Process است و مواردی را که لازم است، در اختیار شما قرار می‌دهد. در این شیوه لازم نیست شما مرتبا به ارسال پیام بپردازید، بلکه این ارتباطات را ماژول remote فراهم می‌کند. این مورد شبیه به سیستم RMI در جاواست.

برای استفاده از remote در فایل html، کدهای زیر را در تگ اسکریپت اضافه می‌کنیم:
  const remote=require("electron").remote;
    const dialog=remote.dialog;
اینبار هم مانند قسمت قبلی، کدها را به شیوه دیگری انتساب دادیم. قصد ما از تغییر این رویه این است که با انواع حالت‌های انتساب اشیاء، آشنا شویم. بعد از آن توابع زیر را اضافه می‌کنیم:
  function OpenDialog()
    {
      dialog.showOpenDialog({
        title:'باز کردن فایل متنی',
         properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
        ,filters:[
        {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
        {name:'جهت تست' , extensions:['doc','docx']}
         ]
      },
        (filename)=>{
          if(filename===undefined)
             return;
             var content=  fs.readFileSync(String(filename),'utf8');
             document.getElementById("TextFile").value=content;
    });
    }

    function SaveDialog()
    {
      dialog.showSaveDialog({
        title:'باز کردن فایل متنی',
         properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
        ,filters:[
        {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
         ]
      },
        (filename)=>{
          if(filename===undefined)
             return;
             var content=document.getElementById("TextFile").value;
             fs.writeFileSync(String(filename),content,'utf8');
       });
    }
برای استفاده از این توابع، کدهای زیر را نیز به فایل اضافه می‌کنیم تا دکمه‌های open و save به صفحه اضافه شوند:
<button onclick="OpenDialog();" > Open File</button>
<button onclick="SaveDialog();" > Save File</button>
حالا برنامه را اجرا و تست کنید.

عبارت remote شامل متدهای فراوانی است که تعدادی از آن‌ها را بر می‌شماریم:
remote.getCurrentWindow()
شیء BrowserWindow صفحه جاری را باز می‌گرداند.

remote.getCurrentWebContents()
شیء webContents صفحه جاری را باز می‌گرداند.

remote.getGlobal(name)
این متد، دسترسی به شیء global را داراست و یکی از اشیاء ارتباطی بین Main Process و RenderProcess است که می‌تواند هر نوع داده‌ای را جابجا نماید. برای مشاهده بهتر از نحوه کارکرد این متد کد زیر را مشاهده نمایید:
Main Process
global.testData={year:1395};

Render Process
alert(remote.getGlobal("testData").year);
از این پس هر موقع renderProcess به این کد برسد، پیام 1395 را روی صفحه نمایش خواهد داد.

remote.process
شیء، process را از main process دریافت می‌کند و با کد زیر برابر است. ولی مزیت این متد این است که از کش نیز استفاده می‌نماید.
remote.getGlobal('process')

در مورد شیء process باید گفت که شامل خصوصیات و متدهایی در مورد پروسه اصلی اپلیکیشن می‌باشد. این اطلاعات مثل دریافت شماره نسخه الکترون، شماره نسخه کرومیوم، دریافت اطلاعات حافظه در مورد پروسه اپلیکیشن و حتی دریافت اطلاعات حافظه در مورد کل سیستم و ... می‌شود.
مطالب
معرفی Async Parallel.ForEach در دات نت 6
عموما زمانیکه می‌خواهیم تمام وظایف مدنظر، به صورت موازی اجرا شوند، آن‌ها را Task.WhenAll می‌کنیم. برای مثال 10 هزار درخواست HTTP را به صورت وظایفی، WhenAll می‌کنیم و ... در این حالت ... سرور ریموت، IP شما را خواهد بست! چون کنترلی بر روی تعداد وظیفه‌ی در حالت اجرای موازی وجود ندارد و یک چنین عملی، شبیه به یک حمله‌ی DDOS عمل می‌کند! برای مدیریت بهتر یک چنین مواردی، در دات نت 6 متدهای Parallel.ForEachAsync ارائه شده‌اند تا دیگر نیازی به استفاده از راه‌حل‌های ثالثی که عموما آنچنان بهینه هم نیستند، نباشد.
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, CancellationToken cancellationToken, Func<TSource, CancellationToken, ValueTask> body)
public static Task ForEachAsync<TSource>(IAsyncEnumerable<TSource> source, ParallelOptions parallelOptions, Func<TSource, CancellationToken, ValueTask> body)
این مجموعه متدها از ValueTaskها بجای Taskها استفاده می‌کند تا سربار ایجاد Taskها در حلقه‌ها کاهش یابد. همچنین در اینجا degree of parallelism به صورت پیش‌فرض به تعداد هسته‌های سی‌پی تنظیم شده‌است (Environment.ProcessorCount)؛ چون عموما توسعه دهنده‌ها نمی‌دانند که چه عددی را باید برای آن انتخاب کنند. هر چند امکان تنظیم دستی آن‌ها هم وجود دارد (یکی از مهم‌ترین مشکلات کار با WhenAll).

یک مثال: در اینجا می‌خواهیم به صورت موازی، مشخصات کاربرانی از Github را توسط HttpClient دریافت کنیم. هر بار هم فقط می‌خواهیم سه وظیفه اجرا شوند و نه بیشتر
using System.Net.Http.Headers;
using System.Net.Http.Json;
 
var userHandlers = new []  { "users/VahidN", "users/shanselman", "users/jaredpar", "users/davidfowl" };
 
using HttpClient client = new()
{
    BaseAddress = new Uri("https://api.github.com"),
};
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("DotNet", "6"));
 
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = 3 };
 
await Parallel.ForEachAsync(userHandlers, parallelOptions, async (uri, token) =>
{
    var user = await client.GetFromJsonAsync<GitHubUser>(uri, token); 
    Console.WriteLine($"Name: {user.Name}\nBio: {user.Bio}\n");
});
 
public class GitHubUser
{
    public string Name { get; set; }
    public string  Bio { get; set; }
}
در این مثال، نمونه‌ای از کارکرد متد جدید Parallel.ForEachAsync را مشاهده می‌کنید که اینبار، MaxDegreeOfParallelism آن قابل تنظیم است. یعنی با تنظیم فوق، هربار فقط سه وظیفه به صورت موازی اجرا خواهند شد. البته تنظیم آن به منهای یک، همان حالت WhenAll را سبب خواهد شد؛ یعنی محدودیتی وجود نخواهد داشت.
متد Parallel.ForEachAsync، آرایه‌ای را که باید بر روی آن کار کند، دریافت می‌کند. سپس تنظیمات اجرای موازی آن‌ها را هم مشخص می‌کنیم. در ادامه آن‌ها را در دسته‌های مشخصی، به صورت موازی بر اساس منطقی که مشخص می‌کنیم، اجرا خواهد کرد.


وضعیت امکان اجرای موازی متدهای async همزمان، تا پیش از دات نت 6

<List<T به همراه متد الحاقی ForEach است که می‌تواند یک <Action<T را بر روی المان‌های این لیست، اجرا کند و ... عموما زمانیکه به وظایف async می‌رسیم، به اشتباه مورد استفاده قرار می‌گیرد:
customers.ForEach(c => SendEmailAsync(c));
مثال فوق، با اجرای حلقه‌ی زیر تفاوتی ندارد:
foreach(var c in customers)
{
    SendEmailAsync(c); // the return task is ignored
}
یعنی یک عملیات async، بدون await فراخوانی شده‌است و تا پایان عملیات مدنظر، صبر نخواهد شد. حداقل مشکل آن این است که اگر در این بین استثنایی رخ دهد، هیچگاه متوجه آن نخواهید شد و حتی می‌تواند کل پروسه‌ی برنامه را خاتمه دهد. شاید عنوان کنید که می‌شود این مشکل را به صورت زیر حل کرد:
customers.ForEach(async c => await SendEmailAsync(c));
اما ... این روش هم تفاوتی با قبل ندارد. از این لحاظ که متد ForEach یک <Action<T را دریافت می‌کند که خروجی آن void است. یعنی در نهایت با راه حل دوم، فقط یک async void ایجاد می‌شود که باز هم قابلیت صبر کردن تا پایان عملیات را ندارد. نکته‌ی مهم اینجا است که اجرای موازی آن‌ها توسط متد Parallel.ForEach نیز دقیقا همین مشکل را دارد.
تنها راه حل پذیرفته‌ی شده‌ی چنین عمل async ای، فراخوانی آن‌ها به صورت متداول زیر و بدون استفاده از متد ForEach است:
foreach(var c in customers)
{
   await SendEmailAsync(c);
}
و یا Task.WhenAll کردن آن‌ها، با علم به این موضوع که MaxDegreeOfParallelism آن قابل کنترل نیست (حداقل به صورت استاندارد و بدون نیاز به کتابخانه‌های جانبی). برای مثال بجای نوشتن:
foreach(var o in orders)
{
    await ProcessOrderAsync(o);
}
می‌توان آن‌را به صورت زیر درآورد:
var tasks = orders.Select(o => ProcessOrderAsync(o)).ToList();
await Task.WhenAll(tasks);
در این حالت عملیات ProcessOrderAsync را تبدیل به لیستی از وظایف مدنظر کرده و به متد Task.WhenAll ارسال می‌کنیم تا به صورت موازی اجرا شوند. اما ... اگر 10 هزار Task وجود داشته باشند، کنترلی بر روی تعداد وظایف در حال اجرای موازی وجود نخواهد داشت و این مورد نه تنها سبب بالا رفتن کارآیی نخواهد شد، بلکه می‌تواند سرور را هم با اخلال پردازشی، به علت کمبود منابع در دسترس مواجه کند.

دات نت 6، هم کنترل MaxDegreeOfParallelism را میسر کرده‌است و هم اینکه اینبار نگارش async واقعی Parallel.ForEachAsync را ارائه داده‌است تا دیگر همانند حالت قبلی Parallel.ForEach، به async void‌ها و مشکلات مرتبط با آن‌ها نرسیم.
مطالب
کامپایل پویای کد در دات نت

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

مثال یک:
رشته زیر را کامپایل کرده و تبدیل به یک فایل exe کنید:

string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";
روش انجام کار به همراه توضیحات مربوطه به صورت کامنت:

using System;
using System.Collections.Generic;
//دو فضای نامی که برای این منظور اضافه شده‌اند
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace compilerTest
{
class Program
{
static void compileIt1()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";

//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);

//تعیین اینکه خروجی یک فایل اجرایی است بعلاوه مشخص سازی محل ذخیره سازی فایل نهایی
CompilerParameters compilerParams = new CompilerParameters
{
OutputAssembly = "D:\\Foo.EXE",
GenerateExecutable = true
};

//عملیات کامپایل در اینجا صورت می‌گیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);

//اگر خطایی وجود داشته باشد نمایش داده خواهد شد
Console.WriteLine("Number of Errors: {0}", results.Errors.Count);
foreach (CompilerError err in results.Errors)
{
Console.WriteLine("ERROR {0}", err.ErrorText);
}
}

static void Main(string[] args)
{
compileIt1();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
مثال 2:
کد مورد نظر را به صورت یک فایل dll کامپایل کنید.
برای این منظور تمامی مراحل مانند قبل است فقط GenerateExecutable ذکر شده به false تنظیم شده و نام خروجی نیز به foo.dll باید تنظیم شود.


مثال 3:
کد مورد نظر را در حافظه کامپایل کرده (خروجی dll یا exe نمی‌خواهیم)، سپس متد SayHello آن را به صورت پویا فراخوانی نموده و خروجی را نمایش دهید.
در این حالت روش کار همانند مثال 1 است با این تفاوت که GenerateInMemory = true و GenerateExecutable = false تنظیم می‌شوند. همچنین جهت دسترسی به متد کلاس ذکر شده،‌ از قابلیت‌های ریفلکشن موجود در دات نت فریم ورک استفاده خواهد شد.

using System;
using System.Collections.Generic;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;

namespace compilerTest
{
class Program
{
static void compileIt2()
{
//سورس کد ما جهت کامپایل
string source =
@"
namespace Foo
{
public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
}
}
}
";

//تعیین نگارش کامپایلر مورد استفاده
Dictionary<string, string> providerOptions = new Dictionary<string, string>
{
{"CompilerVersion", "v3.5"}
};
//تعیین اینکه کد ما سی شارپ است
CSharpCodeProvider provider = new CSharpCodeProvider(providerOptions);

//نحوه تعیین مشخص سازی کامپایل در حافظه
CompilerParameters compilerParams = new CompilerParameters
{
GenerateInMemory = true,
GenerateExecutable = false
};

//عملیات کامپایل در اینجا صورت می‌گیرد
CompilerResults results = provider.CompileAssemblyFromSource(compilerParams, source);

// اگر خطایی در کامپایل وجود نداشت متد دلخواه را فراخوانی می‌کنیم
if (results.Errors.Count == 0)
{
//استفاده از ریفلکشن برای دسترسی به متد و فراخوانی آن
Type type = results.CompiledAssembly.GetType("Foo.Bar");
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(null, null);
}
}


static void Main(string[] args)
{
compileIt2();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
نکته: نحوه‌ی استفاده از اسمبلی‌های دیگر در رشته سورس کد خود
مثال:
اگر رشته سورس ما به صورت زیر بوده و از اسمبلی System.Drawing.Dll نیز کمک گرفته باشد،‌

string source =
@"
namespace Foo
{

public class Bar
{
static void Main(string[] args)
{
Bar.SayHello();
}

public static void SayHello()
{
System.Console.WriteLine(""Hello World"");
var r = new System.Drawing.Rectangle(0,0,100,100);
System.Console.WriteLine(r);
}
}
}
";
هنگام کامپایل آن توسط روش مثال یک، با خطای زیر مواجه خواهیم شد.

Number of Errors: 1
ERROR The type or namespace name 'Drawing' does not exist in the namespace 'System' (are you missing an assembly reference?)

برای رفع این مشکل و معرفی این اسمبلی،‌ سطر زیر باید پس از تعریف compilerParams اضافه شود.

compilerParams.ReferencedAssemblies.Add("System.Drawing.Dll");
اکنون کد کامپایل شده و مشکلی نخواهد داشت.
نمونه‌ای دیگر از این دست، استفاده از LINQ می‌باشد. در این حالت اسمبلی System.Core.Dll نیز به روش ذکر شده باید معرفی گردد تا مشکلی در کامپایل کد رخ ندهد.


کاربردها:
1- استفاده در ابزارهای تولید کد (برای مثال در برنامه Linqer از این قابلیت استفاده می‌شود)
2- استفاده‌های امنیتی (ایجاد روش‌های تولید یک سریال به صورت پویا و کامپایل پویای کد مربوطه در حافظه‌ای محافظت شده)
3- استفاده جهت مقاصد محاسباتی پیشرفته
4- دادن اجازه‌ی کد نویسی به کاربران برنامه‌ی خود (شبیه به سیستم‌های ماکرو و اسکریپت نویسی موجود)
و ...

مطالب
کدامیک از بسته‌های NET Core. را باید دریافت کنیم؟
زمانیکه به صفحه‌ی دریافت نگارش‌های مختلف NET Core. مراجعه می‌کنیم، بسته‌های مختلفی از یک نگارش قابل مشاهده هستند و در بدو امر واضح نیست که کدامیک را باید دریافت کرد. در این مطلب تفاوت‌های بین این بسته‌ها را مرور خواهیم کرد.


کدام نگارش‌های NET Core. بر روی سیستم شما نصب هستند؟

پیش از انجام هرکاری نیاز است بررسی کنیم کدامیک از بسته‌های ارائه شده، بر روی سیستم جاری نصب هستند. برای انجام اینکار دستور زیر را در خط فرمان صادر کنید:
 dotnet --info
اگر این دستور کار نکرد و خطایی را دریافت کردید، یعنی NET Core. اصلا بر روی سیستم شما نصب نیست. برنامه dotnet.exe جزئی از runtime نصب شده‌است و به صورت خودکار به path سیستم اضافه می‌شود. به همین جهت است که در صورت نصب آن، فرمان dotnet در هر مسیری قابل اجرا است.
Runtime تنها ویژگی‌های اساسی جهت اجرای برنامه‌های از پیش کامپایل شده‌ی NET Core. را با اجرای فرمانی مانند dotnet mydll.dll و یا اجرای دستور dotnet --info برای دریافت اطلاعاتی از جزئیات این ویژگی‌ها، به همراه دارد. اما برای کار با سورس کدها، build، publish و هر کار دیگری با آن‌ها، حتما باید SDK نیز نصب شود.

خروجی فرمان فوق بر روی سیستم من چنین چیزی است:
 C:\Users\Vahid>dotnet --info
.NET Core SDK (reflecting any global.json):
 Version: 2.1.301
 Commit: 59524873d6

Runtime Environment:
 OS Name:   Windows
 OS Version:  10.0.17134
 OS Platform: Windows
 RID: win10-x64
 Base Path: C:\Program Files\dotnet\sdk\2.1.301\

Host (useful for support):
  Version: 2.1.1
  Commit:  6985b9f684

.NET Core SDKs installed:
  2.1.300 [C:\Program Files\dotnet\sdk]
  2.1.301 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.NETCore.App 2.1.0 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 2.1.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]

To install additional .NET Core runtimes or SDKs:
  https://aka.ms/dotnet-download
اولین شماره نگارش نمایش داده شده‌ی در این لیست (2.1.301)، شماره نگارش SDK فعال است. سپس شماره نگارش 2.1.1 به معنای شماره نگارش Runtime فعال بر روی سیستم است که هاست dotnet.exe به شمار می‌رود. سپس لیست SDKها و Runtimeهای نصب شده‌ی بر روی سیستم را نمایش می‌دهد.
باید دقت داشت که بر روی یک سیستم می‌توان چندین SDK و چندین Runtime مختلف را نصب کرد و هر پروژه از شماره نگارش خاصی استفاده کند. شماره نگارش runtime استفاده شده‌ی در پروژه‌ها در فایل csproj، توسط مدخل زیر مشخص می‌شود:
 <TargetFramework>netcoreapp2.1</TargetFramework>
در مورد SDK اینطور نیست و همواره از آخرین SDK نصب شده (همان شماره نگارش SDK فعال فوق) استفاده می‌شود، مگر اینکه فایل ویژه‌ای به نام global.json را در ریشه‌ی اصلی solution قرار دهید؛ با این محتوای فرضی:
 {
  "sdk": {
        "version": "2.1.300-rc.31211"
  }
}
در این حالت پروژه‌ی جاری را وادار می‌کنید بجای استفاده‌ی از آخرین SDK نصب شده‌ی بر روی سیستم، از نگارش SDK خاصی استفاده کند.
البته در اکثر موارد نیازی به انجام این کار نیست؛ چون SDK، با تمام نگارش‌های قبلی سازگار است و همواره استفاده‌ی از آخرین SDK نصب شده توصیه می‌شود. به همین جهت فایل global.json را پس از ایجاد یک solution جدید مشاهده نمی‌کنید؛ مگر اینکه خودتان به دلایل خاصی آن‌را اضافه و مقید نمائید.


تفاوت بسته‌های مختلف قابل دریافت NET Core. در چیست؟

زمانیکه برای دریافت آخرین نگارش NET Core. به سایت آن مراجعه می‌کنیم، به ازای هر نگارش، یک چنین لیستی قابل مشاهده است:
• .NET Core Runtime
• .NET Core SDK
• .NET Core Hosting Bundle
• Visual Studio
• ASP.NET Core Installer
و اکنون سؤالی که مطرح می‌شود این است: کدامیک را باید دریافت کرد؟

Visual Studio

اگر کاربر ویندوز هستید، با نصب آخرین نگارش Visual Studio، می‌توانید به همراه آن، آخرین نگارش SDK ،runtime و اجزای هاست برنامه‌های ASP.NET Core بر روی IIS را نیز بر روی سیستم خود نصب کنید.


NET Core SDK.

هدف از ارائه‌ی بسته‌ی SDK، انجام فرآیندهای build‌، اجرا و مدیریت امور مرتبط با NET Core.، بدون استفاده از Visual Studio و بر روی تمام سیستم عامل‌های پشتیبانی شده‌است. زمانیکه یک بسته‌ی SDK را نصب می‌کنید، به همراه آن این موارد نیز نصب می‌شوند:
• .NET Core SDK 
• .NET Core Runtime 
• ASP.NET Core Runtime
به همین جهت حجم آن از بسته‌ی تکی runtime بیشتر است و با نصب آن دیگر نیازی به دریافت مجزای بسته‌های runtime نیست.

بنابراین دلیل نصب آن می‌تواند شامل یکی از موارد زیر باشد:
 - بر روی سیستمی که در حال توسعه‌ی برنامه‌های مبتنی بر NET Core. هستید. این تمام چیزی است که به آن نیاز دارید.
 - بر روی سروری که نیاز است دستور dotnet را برای انجام فرآیندهای build/publish اجرا کند.


NET Core Runtime.

بسته‌های Runtimes، کوچکترین بسته‌ی ممکن در این لیست هستند و هدف از آن‌ها صرفا اجرای برنامه‌های کامپایل شده‌ی NET Core. در سکوهای کاری مختلف پشتیبانی شده‌ی توسط آن است.
باید دقت داشت که اگر برنامه‌ی شما از «ASP.NET Core meta package» استفاده می‌کند، این بسته در runtime لحاظ نشده‌است و در یک چنین حالتی باید بسته‌ی ASP.NET Core را به صورت جداگانه دریافت و نصب کنید. هرچند اگر از این متاپکیج‌ها استفاده نکنید و بسته‌های مورد نیاز را به صورت مستقیم به برنامه‌ی خود اضافه کنید، این بسته‌ها جزئی از فایل‌های publish نهایی بوده و در این حالت برنامه توسط بسته‌ی runtime نیز قابل اجرا است.
در این حالت برنامه‌ی dotnet بجز اجرای برنامه‌ها و ارائه‌ی اطلاعاتی در مورد خود آن، کارهای دیگری را مانند build و یا publish، نمی‌تواند انجام دهد و برنامه در این حالت باید کاملا از پیش کامپایل شده باشد.

بنابراین دلیل نصب آن می‌تواند شامل یکی از موارد زیر باشد:
- برای اجرای برنامه‌های از پیش کامپایل شده‌ای که به همراه تمام وابستگی‌های مورد نیاز هم هستند.
- برای اجرای برنامه‌های وبی که از ASP.NET Meta packages استفاده نمی‌کنند


ASP.NET Core Installer

همانطور که در توضیحات بسته‌ی runtime عنوان شد، این بسته، متاپکیج‌های ASP.NET Core را به همراه ندارد. اگر به آن‌ها نیاز دارید، باید آن‌ها را به صورت جداگانه توسط ASP.NET Core installer نصب کنید که شامل این موارد است:
- The ASP.NET Runtime Meta Packages
- Microsoft.AspNetCore.App
- Microsoft.AspNetCore.All
 
NET Core Windows Hosting Pack.

نصب این بسته برای هاست برنامه‌های ASP.NET Core در ویندوز و بر روی IIS ضروری است و شامل این اجزا می‌شود:
- 32 bit and 64 .NET Core Runtimes
- ASP.NET Runtime Packages (Microsoft.AspNetCode.App/All)
- IIS Hosting Components
بنابراین این بسته شامل تمام موارد یاد شده‌است منهای قابلیت‌های SDK برای build و publish برنامه‌ها.



بنابراین به صورت خلاصه

برای سرورها این موارد را نصب کنید:
- در ویندوز: Windows Server Hosting Bundle
- برای Mac و لینوکس:  .NET Core Runtime + ASP.NET Core Runtimes

برای سیستم توسعه‌ی شخصی این موارد را نصب کنید:
- SDK
- اگر از ویندوز استفاده می‌کنید: Visual Studio هم به همراه SDK نصب می‌شود.

برای اجرای برنامه‌های از پیش کامپایل شده که به همراه تمام وابستگی‌های مورد نیاز هم هستند:
- تنها Runtime را نصب کنید.
اگر این برنامه‌ی از پیش کامپایل شده از ASP.NET Runtime Meta packages استفاده می‌کند:
- ASP.NET Runtimes را نیز نصب کنید.