نظرات مطالب
مشکل در جابجایی پروژه های svn
این مطلب امروز به درد من خورد!
پروژه iTextSharp محل مخزنش رو عوض کرده یا شاید هم Source forge دست به تغییر و تحول زده. محل جدید برای checkout شده:
svn checkout http://svn.code.sf.net/p/itextsharp/code/trunk itextsharp-code
و با uuid جدید زیر
820d3149-562b-4f88-9aa4-a8e61a3485cf
برای relocate مخزن قبلی به محل جدید حتما نیاز است این uuid را در بانک اطلاعاتی فوق ویرایش کرد.
نظرات اشتراک‌ها
تبدیلگر ایران سیستم به یونیکد
سلام
متاسفانه هنوز رشته ترکیبی از عدد و حروف انگلیسی در تبدیل برعکس میشه، مثلا وقتی دارم Rb0027 بعد از تبدیل میشه 0027bR . من اومدم قبل از تبدیل برعکسش کردم که درست بشه ورودی بهش 0027bR دادم و بعد از تبدیل همون Rb0027 که میخواستم رو تو سایت بیمه نمایش داد.
نظرات مطالب
طریقه بررسی صحت کدملی به کمک متدهای الحاقی
این نوع کلاس‌ها هم تماما کمکی هستند. به همان روش سابق نام کلاس نام متد هم قابل دسترسی‌اند. به علاوه، به این ترتیب، علاوه بر روش قدیمی فراخوانی، حالت فراخوانی fluent هم میسر می‌شود که از دیدگاه زبان انگلیسی خواندنش ساده‌تر است و روان‌تر.
نظرات مطالب
اجرای وظایف زمان بندی شده با Quartz.NET - قسمت اول
این کتابخانه کمی ناشناخته است و مطلب زیادی در موردش پیدا نکردم، آن چیزی هم که هست بیشتر انگلیسی است که بیشتر گیج کننده است.
اگر لطف کنید دقیق‌تر (با جزییات) بفرمایید چه کار باید بکنم ممنون میشوم .
یا مثالی با توجه به توضیحی که دادم بگذارید خیلی ممنون میشوم.
مطالب
مشکلات طراحی API مرتبط با iTextSharp

کتابخانه‌ی iTextSharp،‌ یا همان برگردان iText جاوا،‌ انصافا اینقدر حرف برای گفتن دارد که یک کتاب 600 صفحه‌ای برای آن چاپ شده است، اما ... در حین استفاده از آن مشکل زیر (که به شکل وسیعی در قسمت‌های مختلف آن وجود دارد) قابل هضم نیست:
یکی از مواردی را که در حین طراحی یک API خوب باید در نظر گرفت، کمک به استفاده کننده در عدم بکارگیری Magic numbers است. حالا این Magic numbers یعنی چی؟
برای مثال قطعه کد زیر را در نظر بگیرید:
new Font(baseFont, 10, 0, BaseColor.BLACK)
مقدار پارامتر 3 در اینجا بی‌معنا است، مگر اینکه به مستندات مربوطه مراجعه کنیم.
نگارش بهبود یافته کد فوق به شرح زیر است:
new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK)
کمی بهتر شد، اینبار ثوابت به یک کلاس منتقل شده‌اند و به این ترتیب معنای مقدار بکارگرفته شده بدون نیاز به مستندات متد فوق قابل درک است. اما این روش هم (یعنی همان روش متداول iTextSharp) دو مشکل مهم دارد:
  • استفاده کننده محدودیتی در بکارگیری مقادیر ندارد، چون آرگومان‌ها از نوع int معرفی شده‌اند. ممکن است اشتباهی رخ دهد.
  • باز هم نیاز است به مستندات کتابخانه مراجعه کرد، زیرا نوع int هیچ نوع منوی intellisense خاصی را ظاهر نمی‌کند.
راه درست، استفاده از enum است بجای یک کلاس ساده که فقط یک سری از ثوابت در آن تعریف شده‌اند. نوع int را باید با enum زیر جایگزین کرد (یا ... بهتر است اینگونه بشود در کتابخانه‌ی اصلی ... روزی!)
public enum PdfFontStyle
{
Normal = 0,
Bold = 1,
Italic = 2,
Underline = 4,
Strikethru = 8,
BoldItalic = Bold | Italic
}
اگر در طراحی آن از ابتدا این روش پی‌گرفته می‌شد، منوی intellisense تبدیل به بهترین مستند این کتابخانه می‌شد.

مطالب
C# 12.0 - Primary Constructors
قابلیتی تحت عنوان Primary Constructors به C# 12 اضافه شده‌است که ... البته جدید نیست! این قابلیت از زمان C# 9، با ارائه‌ی رکوردها، به زبان #C اضافه شد و در طی چند نگارش بعدی، توسعه و تکامل یافت (برای مثال اضافه شدن records for structs به C# 10) تا در C# 12، به کلاس‌های معمولی نیز تعمیم پیدا کرد. این ویژگی را در ادامه با جزئیات بیشتری بررسی می‌کنیم.


Primary Constructors چیست؟

Primary Constructors، قابلیتی است که به C# 12 اضافه شده‌است تا توسط آن بتوان خواص را مستقیما توسط پارامترهای سازنده‌ی یک کلاس تعریف و همچنین مقدار دهی کرد. هدف از آن، کاهش قابل ملاحظه‌ی یکسری کدهای تکراری و مشخص است تا به کلاس‌هایی زیباتر، کم‌حجم‌تر و خواناتر برسیم. برای مثال کلاس متداول زیر را درنظر بگیرید:
public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime HireDate { get; set; }
    public decimal Salary { get; set; }

    public Employee(string firstName, string lastName, DateTime hireDate, decimal salary)
    {
        FirstName = firstName;
        LastName = lastName;
        HireDate = hireDate;
        Salary = salary;
    }
}
در زبان ‍#C، سازنده، متد ویژه‌ای است که در حین ساخت نمونه‌ای از یک کلاس، فراخوانی می‌شود. هدف از آن‌، آغاز و مقدار دهی حالت شیء ایجاد شده‌است که عموما با مقدار دهی خواص آن شیء، انجام می‌شود.
اکنون اگر بخواهیم همین کلاس را با استفاده از ویژگی Primary Constructor اضافه شده به C# 12.0 بازنویسی کنیم، به قطعه کد زیر می‌رسیم:
public class Employee(string firstName, string lastName, DateTime hireDate, decimal salary)
{
    public string FirstName { get; set; } = firstName;
    public string LastName { get; set; } = lastName;
    public DateTime HireDate { get; set; } = hireDate;
    public decimal Salary { get; set; } = salary;
}
و نحوه‌ی نمونه سازی از آن به صورت زیر است:
var employee = new Employee("John", "Doe", new DateTime(2020, 1, 1), 50000);

یک نکته: اگر از Rider و یا ReSharper استفاده می‌کنید، یک چنین Refactoring توکاری جهت سهولت کار، به آن‌ها اضافه شده‌است و به سرعت می‌توان این تبدیلات را توسط آن‌ها انجام داد.




توضیحات:
- متد سازنده در این حالت، به ظاهر حذف شده و به قسمت تعریف کلاس منتقل شده‌است.
- تمام مقدار دهی‌های آغازین موجود در متد سازنده‌ی پیشین نیز حذف شده‌اند و مستقیما به قسمت تعریف خواص، منتقل شده‌اند.
در نتیجه از یک کلاس 15 سطری، به کلاسی 7 سطری رسیده‌ایم که کاهش حجم قابل ملاحظه‌ای را پیدا کرده‌است.

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

نکته 2: استفاده از پارامترهای سازنده‌ی اولیه، صرفا جهت مقدار دهی خواص عمومی یک کلاس، یک code smell هستند! چون می‌توان یک چنین کارهایی را به نحو شکیل‌تری توسط required properties معرفی شده‌ی در C# 11، پیاده سازی کرد.


بررسی تاریخچه‌ی primary constructors

همانطور که در مقدمه‌ی بحث نیز عنوان شد، primary constructors قابلیت جدیدی نیست و برای نمونه به همراه C# 9 و مفهوم جدید رکوردهای آن، ارائه شد:
public record class Book(string Title, string Publisher);
مثال فوق که به positional syntax هم معروف است، به همراه بکارگیری primary constructors است. در اینجا کامپایلر به صورت خودکار، کار تولید کدهای خواص متناظر را که از نوع get و init دار هستند، انجام می‌دهد. در این حالت به علت استفاده از init accessors، پس از نمونه سازی شیءای از آن، دیگر نمی‌توان مقدار خواص متناظر را تغییر داد.
پس از آن در C# 10، این توسعه ادامه یافت و به امکان تعریف record structها، بسط یافت که در اینجا هم قابلیت تعریف primary constructors وجود دارد:
public record struct Color(int R, int G, int B);
که البته در این حالت برخلاف record classها، کامپایلر، کدی را که برای خواص تولید می‌کند، get و set دار است. در اینجا اگر نیاز است به همان حالت خواص get و init دار رسید، می‌توان یک readonly record struct را تعریف کرد.

پس از این مقدمات، اکنون در C# 12 نیز می‌توان primary constructors را به تمام کلاس‌ها و structهای معمولی هم اعمال کرد؛ با این تفاوت که در اینجا برخلاف رکوردها، کدهای خواص‌های متناظر، به صورت خودکار تولید نمی‌شوند و اگر به آن‌ها نیاز دارید، باید آن‌ها را همانند مثال ابتدای بحث، خودتان به صورت دستی تعریف کنید.


primary constructors کلاس‌ها و structهای معمولی، با primary constructors رکوردها یکی نیست

در C# 12 و به همراه معرفی primary constructors مخصوص کلاس‌ها و structهای معمولی آن، از روش متفاوتی برای دسترسی به پارامترهای تعریف شده، استفاده می‌کند که به آن capturing semantics هم می‌گویند. در این حالت پارامترهای تعریف شده‌ی در یک primary constructor، توسط هر عضوی از آن کلاس قابل استفاده‌است که یکی از کاربردهای آن، ساده کردن تعاریف تزریق وابستگی‌ها است. در این حالت دیگر نیازی نیست تا ابتدا یک فیلد را برای انتساب به پارامتر تزریق شده تعریف کرد و سپس از آن فیلد، استفاده نمود؛ مستقیما می‌توان با همان پارامتر تعریف شده، در متدها و اعضای کلاس، کار کرد.
برای مثال سرویس زیر را که از تزریق وابستگی‌ها، در سازنده‌ی خود استفاده می‌کند، درنظر بگیرید:
public class MyService
{
    private readonly IDepedent _dependent;
  
    public MyService(IDependent dependent)
    {
        _dependent = dependent;
    }
  
    public void Do() 
    {
        _dependent.DoWork();
    }
}
این کلاس در C# 12 به صورت زیر خلاصه شده و پارامتر dependent تعریف شده‌ی در سازنده‌ی اولیه‌ی آن، به همان شکل و بدون نیاز به کد اضافی، در سایر متدهای این کلاس قابل استفاده‌است:
public class MyService(IDependent dependent)
{
    public void Do() 
    {
        dependent.DoWork();
    }
}

البته مفهوم Captures هم در زبان #C جدید نیست و در ابتدا به همراه anonymous methods و بعدها به همراه lambda expressions، معرفی و بکار گرفته شد. برای مثال درون یک lambda expression، اگر از متغیری خارج از آن lambda expressions استفاده شود، کامپایلر یک capture از آن متغیر را تهیه کرده و استفاده می‌کند.

بنابراین به صورت خلاصه primary constructors در رکوردها، با هدف تعریف خواص عمومی فقط خواندنی، ارائه شدند؛ اما primary constructors ارائه شده‌ی در C# 12 که اینبار قابل اعمال به کلاس‌ها و structs معمولی است، بیشتر هدف ساده سازی تعریف کدهای تکراری private fields را دنبال می‌کند. برای نمونه این کدی است که کامپایلر برای primary constructor مثال ابتدای بحث تولید می‌کند و در اینجا نحوه‌ی تولید خودکار این فیلدهای خصوصی را مشاهده می‌کنید:
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace CS8Tests
{
  [NullableContext(1)]
  [Nullable(0)]
  public class Employee
  {
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string <FirstName>k__BackingField;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string <LastName>k__BackingField;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private DateTime <HireDate>k__BackingField;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private Decimal <Salary>k__BackingField;

    public Employee(string firstName, string lastName, DateTime hireDate, Decimal salary)
    {
      this.<FirstName>k__BackingField = firstName;
      this.<LastName>k__BackingField = lastName;
      this.<HireDate>k__BackingField = hireDate;
      this.<Salary>k__BackingField = salary;
      base..ctor();
    }

    public string FirstName
    {
      [CompilerGenerated] get
      {
        return this.<FirstName>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<FirstName>k__BackingField = value;
      }
    }

    public string LastName
    {
      [CompilerGenerated] get
      {
        return this.<LastName>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<LastName>k__BackingField = value;
      }
    }

    public DateTime HireDate
    {
      [CompilerGenerated] get
      {
        return this.<HireDate>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<HireDate>k__BackingField = value;
      }
    }

    public Decimal Salary
    {
      [CompilerGenerated] get
      {
        return this.<Salary>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<Salary>k__BackingField = value;
      }
    }
  }
}
بنابراین آیا پارامترهای سازنده‌ی اولیه، به صورت خواص تعریف می‌شوند و قابلیت تغییر میدان دید آن‌ها میسر است؟ پاسخ: خیر. این پارامترها توسط کامپایلر، به صورت فیلدهای خصوصی در سطح کلاس، تعریف و استفاده می‌شوند. یعنی تمام اعضای کلاس، البته منهای سازنده‌های ثانویه، به این پارامترها دسترسی دارند. همچنین، این تولید کد هم بهینه‌است و صرفا برای پارامترهایی انجام می‌شود که واقعا در کلاس استفاده شده باشند؛ درغیر اینصورت، فیلد خصوصی متناظری برای آن‌ها تولید نخواهد شد.

یک نکته: برای مشاهده‌ی یک چنین کدهایی می‌توانید از منوی Tools->IL Viewer برنامه‌ی Rider استفاده کرده و در برگه‌ی ظاهر شده، گزینه‌ی #Low-Level C آن‌را انتخاب نمائید.


امکان تعریف سازنده‌های دیگر، به همراه سازنده‌ی اولیه

اگر به کدهای #Low-Level C تولیدی فوق دقت کنید، این کلاس، به همراه یک سازنده‌ی خالی بدون پارامتر (parameter less constructor) نیست و سازنده‌ی پیش‌فرضی (default constructor) برای آن درنظر گرفته نشده‌است ... اما اگر کلاسی به همراه یک primary constructor تعریف شد، می‌توان با استفاده از واژه‌ی کلیدی this، سازنده‌ی ثانویه‌ای را هم برای آن تعریف کرد:
public class Person(string firstName, string lastName) 
{
    public Person() : this("John", "Smith") { }
    public Person(string firstName) : this(firstName, "Smith") { }
    public string FullName => $"{firstName} {lastName}";
}
در اینجا نحوه‌ی تعریف یک Default constructor بدون پارامتر را هم ملاحظه می‌کنید.


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

مثال زیر را درنظر بگیرید که در آن کلاس مشتق شده‌ی از کلاس User، یک سازنده‌ی اولیه را تعریف کرده:
public class User
{
    public User(string firstName, string lastName) { }
}

public class Editor(string firstName, string lastName) : User
{
}
در این حالت برنامه با خطای «Base class 'CS8Tests.User' does not contain parameterless constructor» کامپایل نمی‌شود. عنوان می‌کند که اگر کلاس مشتق شده می‌خواهد سازنده‌ی اولیه‌ای داشته باشد، باید کلاس پایه را به همراه یک سازنده‌ی پیش‌فرض بدون پارامتر تعریف کنید.
البته این محدودیت با structها وجود ندارد؛ چون structها، value type هستند و همواره به صورت پیش‌فرض، به همراه یک سازنده‌ی پیش فرض بدون پارامتر، تولید می‌شوند.
یک مثال: قطعه کد متداول ارث‌بری زیر را درنظر بگیرید که در آن، کلاس مشتق شده به کمک واژه‌ی کلید base، امکان تعریف سازنده‌ی جدیدی را یافته و یکی از پارامترهای سازنده‌ی کلاس پایه را مقدار دهی می‌کند:
public class Automobile
{
    public Automobile(int wheels, int seats)
    {
        Wheels = wheels;
        Seats = seats;
    }

    public int Wheels { get; }
    public int Seats { get; }
}

public class Car : Automobile
{
    public Car(int seats) : base(4, seats)
    {
    }
}
این تعاریف در C# 12 به صورت زیر خلاصه می‌شوند:
public class Automobile(int wheels, int seats)
{
    public int Wheels { get; } = wheels;
    public int Seats { get; } = seats;
}

public class Car(int seats) : Automobile(4, seats);

و یا یک نمونه مثال دیگر آن به صورت زیر است که در آن، ذکر بدنه‌ی کلاس در C# 12، الزامی ندارد:
public class MyBaseClass(string s); // no body required

public class Derived(int i, string s, bool b) : MyBaseClass(s)
{
    public int I { get; set; } = i;
    public string B => b.ToString();
}


توصیه به پرهیز از double capturing

با مفهوم capture در این مطلب آشنا شدیم. در مثال زیر دوبار از پارامتر سازنده‌ی age، در دو قسمت عمومی شده، استفاده شده‌است:
public class Human(int age)
{
    // initialization
    public int Age { get; set; } = age;

    // capture
    public string Bio => $"My age is {age}!";
}
در این حالت ممکن است استفاده کننده در طول برنامه، با وضعیت ناخواسته‌ی زیر مواجه شود:
var p = new Human(42);
Console.WriteLine(p.Age); // Output: 42
Console.WriteLine(p.Bio); // Output: My age is 42!

p.Age++;
Console.WriteLine(p.Age); // Output: 43
Console.WriteLine(p.Bio); // Output: My age is 42! // !
در اینجا پس از افزودن مقداری به خاصیت عمومی Age، زمانیکه به مقدار عبارت Bio مراجعه می‌شود، خروجی قبلی را دریافت می‌کنیم!
درک بهتر آن، نیاز به #Low-Level C کلاس Human را دارد:
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace CS8Tests
{
  [NullableContext(1)]
  [Nullable(0)]
  public class Human
  {
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <age>P;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <Age>k__BackingField;

    public Human(int age)
    {
      this.<age>P = age;
      this.<Age>k__BackingField = this.<age>P;
      base..ctor();
    }

    public int Age
    {
      [CompilerGenerated] get
      {
        return this.<Age>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<Age>k__BackingField = value;
      }
    }

    public string Bio
    {
      get
      {
        DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(11, 1);
        interpolatedStringHandler.AppendLiteral("My age is ");
        interpolatedStringHandler.AppendFormatted<int>(this.<age>P);
        interpolatedStringHandler.AppendLiteral("!");
        return interpolatedStringHandler.ToStringAndClear();
      }
    }
  }
}
همانطور که مشاهده می‌کنید، کامپایلر، پارامتر age را دوبار، جداگانه capture کرده‌است:
public Human(int age)
{
   this.<age>P = age;
   this.<Age>k__BackingField = this.<age>P;
   base..ctor();
}
به همین جهت است که ++p.Age، فقط بر روی یکی از فیلدهای capture شده تاثیر داشته و بر روی دیگری خیر. به این مورد، double capturing گفته می‌شود و توصیه شده از آن پرهیز کنید و بجای استفاده‌ی دوباره از پارامتر age، از خود خاصیت Age استفاده نمائید.
مطالب
C# 12.0 - Interceptors
به C# 12 و دات‌نت 8، ویژگی «آزمایشی» جدیدی به نام Interceptors اضافه شده‌است که به آن «monkey patching» هم می‌گویند. هدف از آن، جایگزین کردن یک پیاده سازی، با پیاده سازی دیگری است. به این ترتیب توسعه دهندگان دات‌نتی می‌توانند فراخوانی متدهایی خاص را ره‌گیری کرده (interception) و سپس آن‌را به فراخوانی یک پیاده سازی جدید، هدایت کنند.


Interceptor چیست؟

از زمان ارائه‌ی NET 8 preview 6 SDK. به بعد، امکان ره‌گیری هر متدی از کدهای برنامه، به دات‌نت اضافه شده‌است؛ به همین جهت از واژه‌ی Interceptor/ره‌گیر در اینجا استفاده می‌شود. خود تیم دات‌نت از این قابلیت در جهت بازنویسی پویای قسمت‌هایی از کدهای زیرساخت دات‌نت که از Reflection استفاده می‌کنند، با نگارش‌های کامپایل شده‌ی مختص به برنامه‌ی شما، کمک می‌گیرند. به این ترتیب سرعت و کارآیی برنامه‌های دات‌نت 8، بهبود قابل ملاحظه‌ای را پیدا کرده‌اند. برای مثال ahead-of-time compilation (AOT) در دات‌نت 8 و ASP.NET Core 8x بر اساس این ویژگی پیاده سازی شده‌است. این ویژگی جدید، مکمل source generators است که در نگارش‌های پیشین دات‌نت ارائه شده بود.


بررسی  Interceptors با تهیه‌ی یک مثال ساده

فرض کنید می‌خواهیم فراخوانی متد GetText زیر را ره‌گیری کرده و سپس آن‌را با نمونه‌ی دیگری جایگزین کنیم:
namespace CS8Tests;

public class InterceptorsSample
{
    public string GetText(string text)
    {
        return $"{text}, World!";
    }
}
برای اینکار ابتدا نیاز است یک فایل جدید را به نام InterceptsLocationAttribute.cs با محتوای زیر به پروژه اضافه کرد:
namespace System.Runtime.CompilerServices;

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class InterceptsLocationAttribute : Attribute
{
    public InterceptsLocationAttribute(string filePath, int line, int character)
    {
    }
}
همانطور که در مقدمه‌ی بحث هم عنوان شد، این ویژگی هنوز آزمایشی است و نهایی نشده و ویژگی فوق نیز هنوز به دات‌نت اضافه نشده‌است. به همین جهت فعلا باید آن‌را به صورت دستی به پروژه اضافه کرد و احتمالا در نگارش‌های بعدی دات‌نت، امضای آن تغییر خواهد کرد ... یا حتی ممکن است بطور کامل حذف شود!

سپس فرض کنید فراخوانی متد GetText در فایل Program.cs برنامه به صورت زیر انجام شده‌است:
using CS8Tests;

var example = new InterceptorsSample();
var text = example.GetText("Hello");
Console.WriteLine(text); //Hello, World!
یعنی متد GetText، در سطر چهارم و کاراکتر 20 ام آن فراخوانی شده‌است. این اعداد مهم هستند!

در ادامه از این اطلاعات در ره‌گیر سفارشی زیر استفاده خواهیم کرد:
using System.Runtime.CompilerServices;

namespace CS8Tests;

public static class MyInterceptor
{
    [InterceptsLocation("C:\\Path\\To\\CS8Tests\\Program.cs", 4, 20)] 
    public static string InterceptorMethod(this InterceptorsSample example, string text)
    {
        return $"{text}, DNT!";
    }
}
این ره‌گیر که به صورت متدی الحاقی برای کلاس InterceptorsSample دربرگیرنده‌ی متد GetText تهیه می‌شود، کار جایگزینی فراخوانی آن‌را در سطر چهارم و کاراکتر 20 ام فایل Program.cs انجام می‌دهد. امضای پارامترهای این متد، باید با امضای پارامترهای متد ره‌گیری شده، یکی باشد.

اکنون اگر برنامه را اجرا کنیم ... با خطای زیر مواجه می‌شویم:
 error CS9137: The 'interceptors' experimental feature is not enabled in this namespace. Add
'<InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces>'
to your project.
عنوان می‌کند که این ویژگی آزمایشی است و باید فایل csproj. را به صورت زیر تغییر داد تا بتوان از آن استفاده نمود:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <!--<NoWarn>Test001</NoWarn>-->
    <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);CS8Tests</InterceptorsPreviewNamespaces>
  </PropertyGroup>
</Project>
اینبار برنامه کامپایل شده و اجرا می‌شود. در این حالت خروجی جدید برنامه، خروجی تامین شده‌ی توسط ره‌گیر سفارشی ما است:
Hello, DNT!


سؤال: آیا ره‌گیری انجام شده، در زمان کامپایل انجام می‌شود یا در زمان اجرا؟

برای این مورد می‌توان به Low-Level C# code تولیدی مراجعه کرد. برای مشاهده‌ی یک چنین کدهایی می‌توانید از منوی Tools->IL Viewer برنامه‌ی Rider استفاده کرده و در برگه‌ی ظاهر شده، گزینه‌ی Low-Level C# آن‌را انتخاب نمائید:
using CS8Tests;
using System;
using System.Runtime.CompilerServices;

[CompilerGenerated]
internal class Program
{
  private static void <Main>$(string[] args)
  {
    Console.WriteLine(new InterceptorsSample().InterceptorMethod("Hello"));
  }

  public Program()
  {
    base..ctor();
  }
}
همانطور که مشاهده می‌کنید، این ره‌گیری و جایگزینی، در زمان کامپایل انجام شده و کامپایلر، به‌طور کامل نحوه‌ی فراخوانی متد GetText اصلی را به متد ره‌گیر ما تغییر داده و بازنویسی کرده‌است.


سؤال: آیا این قابلیت واقعا کاربردی است؟!

اکنون شاید این سؤال مطرح شود که ... واقعا چه کسی قرار است مسیر کامل یک فایل، شماره سطر و شماره ستون فراخوانی متدی را به اینگونه در اختیار سیستم ره‌گیری قرار دهد؟! آیا واقعا این قابلیت، یک قابلیت کاربردی و مناسب است؟!
اینجا است که اهمیت source generators مشخص می‌شود. توسط source generators دسترسی کاملی به syntax trees وجود دارد و همچنین یکسری اطلاعات تکمیلی مانند FilePath و سپس CSharpSyntaxNodeها که دسترسی به داده‌های متد ()GetLocation را دارند که مکان دقیق سطر و ستون‌های فراخوانی‌ها را مشخص می‌کند.


کاربردهای فعلی ره‌گیرها در دات نت 8

در دات نت 8، این موارد با استفاده از ره‌گیرها بهینه سازی شده و سرعت آن‌ها افزایش یافته‌اند:
- فراخوانی‌هایی که تمام اطلاعات آن‌ها در زمان کامپایل فراهم است، مانند Regex.IsMatch(@"a+b+") که از یک الگوی ثابت و مشخص استفاده می‌کند، ره‌گیری شده و پیاده سازی آن با کدی استاتیک، جایگزین می‌شود.
- در ASP.NET Minimal API، استفاده از lambda expressions جهت ارائه‌ی تعاریفی مانند:
app.MapGet("/products", handler: (int? page, int? pageLength, MyDb db) => { ... })
مرسوم است. این نوع فراخوانی‌ها نیز توسط ره‌گیرها برای جایگزینی handler آن‌ها با کدهای استاتیک، جهت بالابردن کارآیی و کاهش تخصیص‌های حافظه انجام می‌شود.
- بهبود کارآیی foreach loops جهت استفاده از ریاضیات برداری و SIMD در صورت امکان.
- بهبود کارآیی تزریق وابستگی‌ها، زمانیکه به تعاریف مشخصی مانند ()<provider.Register<MyService ختم می‌شود.
- بجای استفاده از expression trees در زمان اجرای برنامه، اکنون می‌توان کدهای SQL معادل را در زمان کامپایل برنامه تولید کرد.
- بهبود کارآیی Serializers، زمانیکه از یک نوع مشخص مانند ()<Serialize<MyType استفاده می‌شود و کامپایلر می‌تواند آن‌را با کدهای زمان کامپایل، جایگزین کند.


محدودیت‌های ره‌گیرها در دات‌نت 8

- ره‌گیرهای دات‌نت 8 فقط با متدها کار می‌کنند.
- مسیر ارائه شده حتما باید یک مسیر کامل و مشخص باشد. یعنی اگر این قطعه کد، به سیستم دیگری منتقل شود، کامپایل نخواهد شد و امکان ارائه‌ی مسیرهای نسبی وجود ندارد.
- امضای متدها، حتما باید یکی باشد. یعنی نمی‌توان یک ره‌گیر جنریک را تعریف کرد.
مطالب
EF Code First #14

ردیابی تغییرات در EF Code first

EF از DbContext برای ذخیره اطلاعات مرتبط با تغییرات موجودیت‌های تحت کنترل خود کمک می‌گیرد. این نوع اطلاعات توسط Change Tracker API جهت بررسی وضعیت فعلی یک شیء، مقادیر اصلی و مقادیر تغییر کرده آن در دسترس هستند. همچنین در اینجا امکان بارگذاری مجدد اطلاعات موجودیت‌ها از بانک اطلاعاتی جهت اطمینان از به روز بودن آن‌ها تدارک دیده شده است. ساده‌ترین روش دستیابی به این اطلاعات، استفاده از متد context.Entry می‌باشد که یک وهله از موجودیتی خاص را دریافت کرده و سپس به کمک خاصیت State خروجی آن، وضعیت‌هایی مانند Unchanged یا Modified را می‌توان به دست آورد. علاوه بر آن خروجی متد context.Entry، دارای خواصی مانند CurrentValues و OriginalValues نیز می‌باشد. OriginalValues شامل مقادیر خواص موجودیت درست در لحظه اولین بارگذاری در DbContext برنامه است. CurrentValues مقادیر جاری و تغییر یافته موجودیت را باز می‌گرداند. به علاوه این خروجی امکان فراخوانی متد GetDatabaseValues را جهت بدست آوردن مقادیر جدید ذخیره شده در بانک اطلاعاتی نیز ارائه می‌دهد. ممکن است در این بین، خارج از Context جاری، اطلاعات بانک اطلاعاتی توسط کاربر دیگری تغییر کرده باشد. به کمک GetDatabaseValues می‌توان به این نوع اطلاعات نیز دست یافت.
حداقل چهار کاربرد عملی جالب را از اطلاعات موجود در Change Tracker API می‌توان مثال زد که در ادامه به بررسی آن‌ها خواهیم پرداخت.


کلاس‌های مدل مثال جاری

در اینجا یک رابطه many-to-one بین جدول هزینه‌های اقلام خریداری شده یک شخص و جدول فروشندگان تعریف شده است:

using System;

namespace EF_Sample09.DomainClasses
{
public abstract class BaseEntity
{
public int Id { get; set; }

public DateTime CreatedOn { set; get; }
public string CreatedBy { set; get; }

public DateTime ModifiedOn { set; get; }
public string ModifiedBy { set; get; }
}
}

using System;

namespace EF_Sample09.DomainClasses
{
public class Bill : BaseEntity
{
public decimal Amount { set; get; }
public string Description { get; set; }

public virtual Payee Payee { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample09.DomainClasses
{
public class Payee : BaseEntity
{
public string Name { get; set; }

public virtual ICollection<Bill> Bills { set; get; }
}
}


به علاوه همانطور که ملاحظه می‌کنید، این کلاس‌ها از یک abstract class به نام BaseEntity مشتق شده‌اند. هدف از این کلاس پایه تنها تامین یک سری خواص تکراری در کلاس‌های برنامه است و هدف از آن، مباحث ارث بری مانند TPH، TPT و TPC نیست.
به همین جهت برای اینکه این کلاس پایه تبدیل به یک جدول مجزا و یا سبب یکی شدن تمام کلاس‌ها در یک جدول نشود، تنها کافی است آن‌را به عنوان DbSet معرفی نکنیم و یا می‌توان از متد Ignore نیز استفاده کرد:

using System.Data.Entity;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class Sample09Context : MyDbContextBase
{
public DbSet<Bill> Bills { set; get; }
public DbSet<Payee> Payees { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();

base.OnModelCreating(modelBuilder);
}
}
}



الف) به روز رسانی اطلاعات Context در صورتیکه از متد context.Database.ExecuteSqlCommand مستقیما استفاده شود

در قسمت قبل با متد context.Database.ExecuteSqlCommand برای اجرای مستقیم عبارات SQL بر روی بانک اطلاعاتی آشنا شدیم. اگر این متد در نیمه کار یک Context فراخوانی شود، به معنای کنار گذاشتن Change Tracker API می‌باشد؛ زیرا اکنون در سمت بانک اطلاعاتی اتفاقاتی رخ داده‌اند که هنوز در Context جاری کلاینت منعکس نشده‌اند:

using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;

namespace EF_Sample09
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());

using (var db = new Sample09Context())
{
var payee = new Payee { Name = "فروشگاه سر کوچه" };
var bill = new Bill { Amount = 4900, Description = "یک سطل ماست", Payee = payee };
db.Bills.Add(bill);

db.SaveChanges();
}

using (var db = new Sample09Context())
{
var bill1 = db.Bills.Find(1);
bill1.Description = "ماست";

db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
Console.WriteLine(bill1.Description);

db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
Console.WriteLine(bill1.Description);

db.SaveChanges();
}
}
}
}

در این مثال ابتدا دو رکورد به بانک اطلاعاتی اضافه می‌شوند. سپس توسط متد db.Bills.Find، اولین رکورد جدول Bills بازگشت داده می‌شود. در ادامه، خاصیت توضیحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نیز بار دیگر خاصیت توضیحات اولین رکورد به روز خواهد شد.
اکنون اگر مقدار bill1.Description را بررسی کنیم، هنوز دارای مقدار پیش از فراخوانی db.Database.ExecuteSqlCommand می‌باشد، زیرا تغییرات سمت بانک اطلاعاتی هنوز به Context مورد استفاده منعکس نشده است.
در اینجا برای هماهنگی کلاینت با بانک اطلاعاتی، کافی است متد Reload را بر روی موجودیت مورد نظر فراخوانی کنیم.



ب) یکسان سازی ی و ک اطلاعات رشته‌ای دریافتی پیش از ذخیره سازی در بانک اطلاعاتی

یکی از الزامات برنامه‌های فارسی، یکسان سازی ی و ک دریافتی از کاربر است. برای این منظور باید پیش از فراخوانی متد SaveChanges نهایی،‌ مقادیر رشته‌ای کلیه موجودیت‌ها را یافته و به روز رسانی کرد:

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class MyDbContextBase : DbContext
{
public void RejectChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
entry.State = EntityState.Unchanged;
break;

case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}

public override int SaveChanges()
{
applyCorrectYeKe();
auditFields();
return base.SaveChanges();
}

private void applyCorrectYeKe()
{
//پیدا کردن موجودیت‌های تغییر کرده
var changedEntities = this.ChangeTracker
.Entries()
.Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);

foreach (var item in changedEntities)
{
if (item.Entity == null) continue;

//یافتن خواص قابل تنظیم و رشته‌ای این موجودیت‌ها
var propertyInfos = item.Entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));

var pr = new PropertyReflector();

//اعمال یکپارچگی نهایی
foreach (var propertyInfo in propertyInfos)
{
var propName = propertyInfo.Name;
var val = pr.GetValue(item.Entity, propName);
if (val != null)
{
var newVal = val.ToString().Replace("ی", "ی").Replace("ک", "ک");
if (newVal == val.ToString()) continue;
pr.SetValue(item.Entity, propName, newVal);
}
}
}
}

private void auditFields()
{
// var auditUser = User.Identity.Name; // in web apps
var auditDate = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
// Note: You must add a reference to assembly : System.Data.Entity
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedOn = auditDate;
entry.Entity.ModifiedOn = auditDate;
entry.Entity.CreatedBy = "auditUser";
entry.Entity.ModifiedBy = "auditUser";
break;

case EntityState.Modified:
entry.Entity.ModifiedOn = auditDate;
entry.Entity.ModifiedBy = "auditUser";
break;
}
}
}
}
}


اگر به کلاس Context مثال جاری که در ابتدای بحث معرفی شد دقت کرده باشید به این نحو تعریف شده است (بجای DbContext از MyDbContextBase مشتق شده):
public class Sample09Context : MyDbContextBase
علت هم این است که یک سری کد تکراری را که می‌توان در تمام Contextها قرار داد، بهتر است در یک کلاس پایه تعریف کرده و سپس از آن ارث بری کرد.
تعاریف کامل کلاس MyDbContextBase را در کدهای فوق ملاحظه می‌کنید.
در اینجا کار با تحریف متد SaveChanges شروع می‌شود. سپس در متد applyCorrectYeKe کلیه موجودیت‌های تحت نظر ChangeTracker که تغییر کرده باشند یا به آن اضافه شده‌ باشند، یافت شده و سپس خواص رشته‌ای آن‌ها جهت یکسانی سازی ی و ک، بررسی می‌شوند.


ج) ساده‌تر سازی به روز رسانی فیلدهای بازبینی یک رکورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعیت جاری یک موجودیت

در کلاس MyDbContextBase فوق، کار متد auditFields، مقدار دهی خودکار خواص تکراری تاریخ ایجاد، تاریخ به روز رسانی، شخص ایجاد کننده و شخص تغییر دهنده یک رکورد است. به کمک ChangeTracker می‌توان به موجودیت‌هایی از نوع کلاس پایه BaseEntity دست یافت. در اینجا اگر entry.State آن‌ها مساوی EntityState.Added بود، هر چهار خاصیت یاد شده به روز می‌شوند. اگر حالت موجودیت جاری، EntityState.Modified بود، تنها خواص مرتبط با تغییرات رکورد به روز خواهند شد.
به این ترتیب دیگر نیازی نیست تا در حین ثبت یا ویرایش اطلاعات برنامه نگران این چهار خاصیت باشیم؛ زیرا به صورت خودکار مقدار دهی خواهند شد.


د) پیاده سازی قابلیت لغو تغییرات در برنامه

علاوه بر این‌ها در کلاس MyDbContextBase، متد RejectChanges نیز تعریف شده است تا بتوان در صورت نیاز، حالت موجودیت‌های تغییر کرده یا اضافه شده را به حالت پیش از عملیات، بازگرداند.



مطالب
نحوه نمایش فیلدهای تصویری و همچنین بارگذاری تصاویر از مسیری مشخص در PdfReport
عموما برای نمایش تصاویر در گزارشات، یکی از دو حالت زیر وجود دارد:
الف) مسیر تصاویر موجود در فایل سیستم، در بانک اطلاعاتی ذخیره شده‌اند و قرار است گزارش نهایی در ستونی مشخص شامل این تصاویر باشد.
ب) محتوای بایناری تصاویر در خود بانک اطلاعاتی ذخیره شده‌اند و نیاز است آن‌ها را در گزارشات نمایش دهیم.

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

الف) بارگذاری تصاویر از فایل سیستم

ابتدا مدل زیر را در نظر بگیرید:
namespace PdfReportSamples.Models
{
    public class ImageRecord
    {
        public int Id { set; get; }
        public string ImagePath { set; get; }
        public string Name { set; get; }
    }
}
توسط آن تعدادی رکورد را که ImagePath آن‌ها فایل‌هایی بر روی سیستم هستند، خواهیم ساخت:
using System;
using System.Collections.Generic;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.ImageFilePath
{
    public class ImageFilePathPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SnowyPineTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<ImageRecord>
                                             {
                                                 new ImageRecord
                                                     {
                                                         Id=1,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\01.png",
                                                         Name = "Rnd"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=2,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\02.png",
                                                         Name = "Bug"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=3,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\03.png",
                                                         Name = "Stuff"
                                                     },
                                                 new ImageRecord
                                                     {
                                                         Id=4,
                                                         ImagePath =  AppPath.ApplicationPath + "\\Images\\04.png",
                                                         Name = "Sun"
                                                     }
                                             };
                dataSource.StronglyTypedList(listOfRows);
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(3);
                    column.HeaderCell("شماره");
                    column.ColumnItemsTemplate(t => t.Barcode(new Barcode39()));
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.ImagePath);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("تصویر");
                    column.ColumnItemsTemplate(t => t.ImageFilePath(defaultImageFilePath: string.Empty, fitImages: false));
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<ImageRecord>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("نام");
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptImageFilePathSample.pdf"));
        }
    }
}

توضیحات:
- در متد MainTableDataSource، یک سری رکورد دلخواه را بر اساس تعدادی تصویر مشخص ایجاد کرده‌ایم. این تصاویر در پوشه bin\images مثال‌های PdfReport قرار دارند. سپس آن‌ها را توسط متد dataSource.StronglyTypedList، در اختیار PdfReport قرار داده‌ایم.
- در مرحله بعد، توسط متد MainTableColumns، ستون‌های دلخواه گزارش را ایجاد کرده‌ایم.
دو نکته در اینجا مهم هستند:
الف) برای نمایش یک تصویر از فایل سیستم، فقط کافی است از ColumnItemsTemplate متناظر با آن استفاده کرد (حالت پیش فرض، نمایش متنی اطلاعات است). برای نمونه قالب پیش فرض ImageFilePath به نحو زیر قابل استفاده است:
 column.ColumnItemsTemplate(t => t.ImageFilePath(defaultImageFilePath: string.Empty, fitImages: false));
در اینجا در آرگومان اول آن می‌توان مسیر تصویری را مشخص کرد که در صورت موجود نبودن تصویر مشخص شده در منبع داده، بهتر است نمایش داده شود. اگر string.Empty وارد شد، از این مورد صرفنظر خواهد شد. آرگومان دوم آن مشخص می‌کند که آیا تصویر نمایش داده شده باید با ابعاد سلول متناظر با آن هماهنگ شده و نمایش داده شود یا خیر.
ب) در کتابخانه iTextSharp که پایه PdfReport است، امکان تهیه بارکد هم وجود دارد. برای مثال این بارکدها توسط قالب زیر، قابل استفاده خواهند شد:
 column.ColumnItemsTemplate(t => t.Barcode(new Barcode39()));
تصویری از حاصل این گزارش را در ذیل مشاهده می‌کنید:


ب) بارگذاری محتوای تصاویر از بانک اطلاعاتی

ابتدا کدهای کامل این مثال را در نظر بگیرید:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.DbImage
{
    public class DbImagePdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                            Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.AutumnTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.FitToContent);
             })
             .MainTableDataSource(dataSource =>
             {
                 dataSource.GenericDataReader(
                   providerName: "System.Data.SQLite",
                   connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                   sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate], [thumbnail]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                   parametersValues: new object[] { 10 }
               );
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع کل");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.HeaderCell("ردیف");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("url");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.HeaderCell("آدرس");
                 });

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

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("NumberOfPosts");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     column.HeaderCell("تعداد پست");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("AddDate");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                     column.HeaderCell("تاریخ ثبت");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : ((DateTime)obj).ToString("dd/MM/yyyy HH:mm"));
                     });
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("thumbnail");
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(5);
                     column.HeaderCell("تصویر");
                     column.ColumnItemsTemplate(t => t.ByteArrayImage(defaultImageFilePath: string.Empty, fitImages: false));
                 });
             })
             .Export(export =>
             {
                 export.ToXml();
                 export.ToExcel();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptDbImageSample.pdf"));
        }
    }
}

توضیحات:
بانک اطلاعاتی SQLite موجود در پوشه bin\data سورس PdfReport، حاوی فیلدی است به نام thumbnail که در آن محتوای یک سری تصویر ذخیره شده‌اند.
در اینجا توسط کوئری زیر قصد داریم این اطلاعات را خوانده و نمایش دهیم:
SELECT [url], [name], [NumberOfPosts], [AddDate], [thumbnail]
FROM [tblBlogs]
WHERE [NumberOfPosts]>=@p1
همانطور که ملاحظه می‌کنید، در اینجا نیز تنها کافی است قالب ستون مرتبطی را انتخاب کنیم:
column.ColumnItemsTemplate(t => t.ByteArrayImage(defaultImageFilePath: string.Empty, fitImages: false));
قالب ByteArrayImage، اطلاعات باینری فیلد thumbnail را خوانده و تبدیل به تصاویر قرار داده شده در فایل گزارش نهایی می‌کند. پارامترهای آن، با پارامترهای قالب ImageFilePath که پیشتر توضیح داده شد، یکی هستند. 
مطالب
ASP.NET MVC #9

مروری بر HTML Helpers استاندارد مهیا در ASP.NET MVC

یکی از اهداف وجودی Server controls در ASP.NET Web forms، رندر خودکار HTML است. برای مثال Menu control، TreeView control، GridView و امثال آن کار تولید تگ‌های table، tr و بسیاری موارد دیگر را در پشت صحنه برای ما انجام می‌دهند. اما در ASP.NET MVC، هدف رسیدن به یک markup ساده و تمیز است که 100 درصد بر روی اجزای آن کنترل داشته باشیم و این مورد به صورت ضمنی به این معنا است که در اینجا تمام این HTMLها را باید خودمان تولید کنیم. البته در عمل خیر. یک نمونه از آن‌را در قسمت قبل مشاهده کردیم که چطور می‌توان منطق تولید تگ‌های HTML را کپسوله سازی کرد و بارها مورد استفاده قرار داد. به علاوه فریم ورک ASP.NET MVC نیز به همراه تعدادی HTML helper توکار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غیره که کار تولید تگ‌های HTML ضروری و پایه را برای ما ساده می‌کنند.
یک مثال:
@Html.ActionLink("About us", "Index", "About")

در اینجا از متدی به نام ActionLink استفاده شده است. شیء Html هم وهله‌ای از کلاس HtmlHelper است که در تمام Viewها قابل دسترسی می‌باشد.
در این متد،‌ اولین پارامتر، متن نمایش داده شده به کاربر را مشخص می‌کند، پارامتر سوم، نام کنترلری است که مورد استفاده قرار می‌گیرد و پارامتر دوم، نام متد یا اکشنی در آن است که فراخوانی خواهد شد (البته هر کدام از این HtmlHelperها به همراه تعداد قابل توجهی overload هم هستند).
زمانیکه این صفحه را رندر کنیم، به خروجی زیر خواهیم رسید:
<a href="/About">About us</a>

در این لینک نهایی خبری از متد Index ایی که معرفی کردیم، نیست. چرا؟
متد ActionLink بر اساس تعاریف پیش فرض مسیریابی برنامه، سعی می‌کند بهترین خروجی را ارائه دهد. مطابق تعاریف پیش فرض برنامه، متد Index، اکشن پیش فرض کنترلرهای برنامه است. بنابراین ضرورتی به ذکر آن ندیده است.

مثالی دیگر:
همان کلاس‌های Product و Products قسمت هفتم را در نظر بگیرید (قسمت بررسی «ساختار پروژه مثال جاری» در آن مثال). همچنین به اطلاعات «نوشتن HTML Helpers ویژه، به کمک امکانات Razor» قسمت هشتم هم نیاز داریم.
اینبار می‌خواهیم بجای نمایش لیست ساده‌ای از محصولات،‌ ابتدا نام آن‌ها را به صورت لینک‌هایی در صفحه نمایش دهیم. در ادامه پس از کلیک کاربر روی یک نام، توضیحات بیشتری از محصول انتخابی را در صفحه‌ای دیگر ارائه نمائیم. کدهای View ما اینبار به شکل زیر تغییر می‌کنند:

@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>

@GetProductsList(@Model)

توضیحات:
ابتدا یک helper method را تعریف کرده‌ایم و به کمک Html.ActionLink، از نام و شماره محصول، جهت تولید لینک‌های نمایش جزئیات هر یک از محصولات کمک گرفته‌ایم. بنابراین در کنترلر خود نیاز به متد جدیدی به نام Details خواهیم داشت که پارامتری از نوع ProductNumber را دریافت می‌کند. سپس جزئیات این محصول را یافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومی که در متد ActionLink بکارگرفته شده در اینجا مشاهده می‌کنید، یک anonymously typed object است و توسط آن خواصی را تعریف خواهیم کرد که توسط تعاریف مسیریابی تعریف شده در فایل Global.asax.cs،‌ قابل تفسیر و تبدیل به لینک‌های مرتبط و صحیحی باشد.
اکنون اگر این مثال را اجرا کنیم، اولین لینک تولیدی آن به این شکل خواهد بود:
http://localhost/Home/Details/D123

در اینجا به یک نکته مهم هم باید دقت داشت؛ نام کنترلر به صورت خودکار به این لینک اضافه شده است. بنابراین بهتر است از ایجاد دستی این نوع لینک‌ها خودداری کرده و کار را به متدهای استاندارد فریم ورک واگذار نمود تا بهترین خروجی را دریافت کنیم.
البته اگر الان بر روی این لینک کلیک نمائیم، با پیغام 404 مواجه خواهیم شد. برای تکمیل این مثال، متد Details را به کنترلر تعریف شده اضافه خواهیم کرد:

using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;

namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}

public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}

در متد Details، ابتدا ProductNumber دریافت شده و سپس شیء محصول متناظر با آن، به View این متد، بازگشت داده می‌شود. اگر بر اساس ورودی دریافتی، محصولی یافت نشد، کاربر را به View ایی به نام Error که در پوشه Views/Shared قرار گرفته است، هدایت می‌کنیم.
برای اضافه کردن این View هم بر روی متد کلیک راست کرده و گزینه Add view را انتخاب کنید. چون یک شیء strongly typed از نوع Product را قرار است به View ارسال کنیم (مانند مثال قسمت پنجم)، می‌توان در صفحه باز شده تیک Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب کرد و در قسمت Scaffold template هم Details را انتخاب نمود. به این ترتیب Code generator توکار VS.NET قسمتی از کار تولید View را برای ما انجام داده و بدیهی است اکنون سفارشی سازی این View تولیدی که قسمت عمده‌ای از آن تولید شده است، کار ساده‌ای می‌باشد:

@model MvcApplication5.Models.Product

@{
ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
<legend>Product</legend>

<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>

<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>

<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>

در اینجا کدهای مرتبط با View نمایش جزئیات محصول را مشاهده می‌کنید که توسط VS.NET به صورت خودکار از روی مدل انتخابی تولید شده است.
اکنون یکبار دیگر برنامه را اجرا کرده و بر روی لینک نمایش جزئیات محصولات کلیک نمائید تا بتوان این اطلاعات را در صفحه‌ی بعدی مشاهده نمود.


یک نکته:
اگر سعی کنیم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهیم، به متد Html.ActionLink دسترسی نخواهیم داشت. چرا؟
پیغام خطایی که ارائه می‌شود این است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink' 

به این معنا که در وهله‌ای از شیء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink می‌گردد. در حالیکه ActionLink مورد نظر به کلاس System.Web.Mvc.HtmlHelper مرتبط می‌شود.
یک راه حل آن به صورت زیر است. به هر متد helper یک آرگومان WebViewPage page را اضافه می‌کنیم (به همراه دو فضای نامی که به ابتدای فایل اضافه می‌شوند)

@using System.Web.Mvc
@using System.Web.Mvc.Html

@using MvcApplication5.Models

@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
سپس برای استفاده از آن در یک View خواهیم داشت:
@MyHelpers.GetProductsList(this, @Model)


متد ActionLink و عبارات فارسی

متد ActionLink آدرس‌های وبی را که تولید می‌کند، URL encoded هستند. برای نمونه اگر رشته‌ای که قرار است به عنوان پارامتر به اکشن متد ما ارسال شود، مساوی Hello World است، آن‌را به صورت Hello%20World در صفحه درج می‌کند. البته این مورد مشکلی را در سمت متدهای کنترلرها ایجاد نمی‌کند، چون کار URL decoding خودکار است. اما ... اگر مقداری که قرار است ارسال شود مثلا «مقدار یک» باشد، آدرس تولیدی این شکل را خواهد داشت:

http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83

و اگر این URL encoding انجام نشود، فقط اولین قسمت قبل از فاصله به متد ارسال می‌گردد.
مرورگرهایی مثل فایرفاکس و کروم، مشکلی با نمایش این لینک به شکل اصلی فارسی آن ندارند (حین نمایش، URL decoding را اعمال می‌کنند). اما اگر مرورگر مثلا IE8 باشد، کاربر دقیقا به همین شکل آدرس‌ها را در نوار آدرس مرورگر خود مشاهده خواهد کرد که آنچنان زیبا نیستند.
حل این مشکل، یک نکته کوچک را به همراه دارد. اگر href تولیدی به شکل زیر باشد:

<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>

IE حین نمایش نهایی آن، آن‌را فارسی نشان خواهد داد. حتی زمانیکه کاربر بر روی آن کلیک کند، به صورت خودکار کاراکترهایی را که لازم است encode نماید، به نحو صحیحی در URL نهایی قابل مشاهده در نوار آدرس‌ها ظاهر خواهد کرد. برای مثال %20 را به صورت خودکار اضافه می‌کند و نگرانی از این لحاظ وجود نخواهد داشت که الان بین دو کلمه فاصله‌ای وجود دارد یا خیر (مرورگرهای دیگر هم دقیقا همین رفتار را در مورد لینک‌های داخل صفحه دارند).
خلاصه این توضیحات متد کمکی زیر است:

@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}

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

@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}

ضمن اینکه باید درنظر داشت کلا این نوع طراحی مشکل دارد! برای مثال فرض کنید که در این مثال، جزئیات، نمایش دهنده مطلب ارسالی در یک بلاگ است. یعنی یک سری عنوان و جزئیات متناظر با آن‌ها در دیتابیس وجود دارند. اگر آدرس مطالب به این شکل باشد http://site/blog/details/text، به این معنا است که این text مساوی است با primary key جدول بانک اطلاعاتی. یعنی وبلاگ نویس سایت شما فقط یکبار در طول عمر این برنامه می‌تواند بگوید «سال نو مبارک!». دفعه‌ی بعد به علت تکراری بودن، مجاز به ارسال پیام تبریک دیگری نخواهد بود! به همین جهت بهتر است طراحی را به این شکل تغییر دهید http://site/blog/details/id/text. در اینجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشایند بانک اطلاعاتی و Text هم برای خوشایند موتورهای جستجو در این URL قرار دارند. مطابق تعاریف مسیریابی برنامه، Text فقط حالت تزئینی داشته و پردازش نخواهد شد.
از این نوع ترفندها زیاد به کار برده می‌شوند. برای نمونه به URL مطالب انجمن‌های معروف اینترنتی دقت کنید. عموما یک عدد را به همراه text مشاهده می‌کنید. عدد در برنامه پردازش می‌شود، متن هم برای موتورهای جستجو درنظر گرفته شده است.