مطالب
نوشتن پرس و جو در Entity Framework‌ با استفاده از LINQ To Entity قسمت اول

موجودیت‌های زیر را در نظر بگیرید: 

public class Customer
{
    public Customer()
    {
        Orders = new ObservableCollection<Order>();
    }
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }

    public string FullName
    {
        get
        {
            return Name + " " + Family;
        }
    }

    public virtual IList<Order> Orders { get; set; }
}
public class Product
{
    public Product()
    {
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
public class OrderDetail
{
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    public int Count { get; set; }
    public Guid OrderId { get; set; }
    public int Price { get; set; }

    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }

    public string ProductName
    {
        get
        {
            return Product != null ? Product.Name : string.Empty;
        }
    }
}
public class Order
{
    public Order()
    {
        OrderDetail = new ObservableCollection<OrderDetail>();
    }
    public Guid Id { get; set; }
    public DateTime Date { get; set; }

    public Guid CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
    public virtual IList<OrderDetail> OrderDetail { get; set; }

    public string CustomerFullName
    {
        get
        {
            return Customer == null ? string.Empty : Customer.FullName;
        }
    }

    public int TotalPrice
    {
        get
        {
            if (OrderDetail == null)
                return 0;

            return
                OrderDetail.Where(orderdetail => orderdetail.Product != null)
                .Sum(orderdetail => orderdetail.Price*orderdetail.Count);
        }
    }
}

و نگاشت موجودیت ها: 

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.Id);
        Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        HasKey(p => p.Id);
        Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderDetailConfiguration : EntityTypeConfiguration<OrderDetail>
{
    public OrderDetailConfiguration()
    {
        HasKey(od => od.Id);
        Property(od => od.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderConfiguration: EntityTypeConfiguration<Order>
{
    public OrderConfiguration()
    {
        HasKey(o => o.Id);
        Property(o => o.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}

و برای معرفی موجودیت‌ها به Entity Framwork کلاس StoreDbContext را به صورت زیر تعریف می‌کنیم:

public class StoreDbContext : DbContext
{
    public StoreDbContext()
        : base("name=StoreDb")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CustomerConfiguration());
        modelBuilder.Configurations.Add(new OrderConfiguration());
        modelBuilder.Configurations.Add(new OrderDetailConfiguration());
        modelBuilder.Configurations.Add(new ProductConfiguration());
    }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderDetail> OrderDetails { get; set; }
}

جهت مقدار دهی اولیه به database تستی یک DataBaseInitializer به صورت زیر تعریف می‌کنیم:

public class MyTestDb : DropCreateDatabaseAlways<StoreDbContext>
{
    protected override void Seed(StoreDbContext context)
    {
        var customer1 = new Customer { Name = "Vahid", Family = "Nasiri" };
        var customer2 = new Customer { Name = "Mohsen", Family = "Jamshidi" };
        var customer3 = new Customer { Name = "Mohsen", Family = "Akbari" };

        var product1 = new Product {Name = "CPU", Price = 350000};
        var product2 = new Product {Name = "Monitor", Price = 500000};
        var product3 = new Product {Name = "Keyboard", Price = 30000};
        var product4 = new Product {Name = "Mouse", Price = 20000};
        var product5 = new Product {Name = "Power", Price = 70000};
        var product6 = new Product {Name = "Hard", Price = 250000};

        var order1 = new Order
        {
            Customer = customer1, Date = new DateTime(2013, 1, 1),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 1, Price = product1.Price},
                new OrderDetail {Product = product2, Count = 1, Price = product2.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
            }
        };

        var order2 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 5),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 2, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 4, Price = product3.Price},
            }
        };

        var order3 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 4, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 5, Price = product3.Price},
                new OrderDetail {Product = product5, Count = 6, Price = product5.Price},
            }
        };

        var order4 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
                new OrderDetail {Product = product6, Count = 1, Price = product6.Price},
            }
        };

        var order5 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 12),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product5, Count = 2, Price = product5.Price},
                new OrderDetail {Product = product6, Count = 5, Price = product6.Price},
            }
        };

        context.Customers.Add(customer3);

        context.Orders.Add(order1);
        context.Orders.Add(order2);
        context.Orders.Add(order3);
        context.Orders.Add(order4);
        context.Orders.Add(order5);

        context.SaveChanges();
    }

و در ابتدای برنامه کد زیر را جهت مقداردهی اولیه به Database مان قرار می‌دهیم:

Database.SetInitializer(new MyTestDb());

در انتها ConnectionString را در App.Config به صورت زیر تعریف می‌کنیم:

<connectionStrings>
    <add name="StoreDb" connectionString="Data Source=.\SQLEXPRESS;
Initial Catalog=StoreDBTest;Integrated Security = true" providerName="System.Data.SqlClient"/>
</connectionStrings>

بسیار خوب، حالا همه چیز محیاست برای اجرای اولین پرس و جو:

using (var context = new StoreDbContext())
{
    var query = context.Customers;

    foreach (var customer in query)
    {
        Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                  customer.Name, customer.Family);
    }
}

پرس و جوی تعریف شده لیست تمام Customer‌ها را باز می‌گرداند. query فقط یک "عبارت" پرس و جو هست و زمانی اجرا می‌شود که از آن درخواست نتیجه شود. در مثال بالا این درخواست در اجرای حلقه foreach اتفاق می‌افتد و درست در این لحظه است که دستور SQL ساخته شده و به Database فرستاده می‌شود. EF در این حالت تمام داده‌ها را در یک لحظه باز نمی‌گرداند بلکه این ارتباط فعال است تا حلقه به پایان برسد و تمام داده‌ها از database واکشی شود. خروجی به صورت زیر خواهد بود:

Customer Name: Vahid, Customer Family: Nasiri
Customer Name: Mohsen, Customer Family: Jamshidi
Customer Name: Mohsen, Customer Family: Akbari

نکته: با هر بار درخواست نتیجه از query ، پرس و جوی مربوطه دوباره به database فرستاده می‌شود که ممکن است مطلوب ما نباشد و باعث افت سرعت شود. برای جلوگیری از تکرار این عمل کافیست با استفاده از متد ToList پرس و جو را در لحظه تعریف به اجرا در آوریم
var customers = context.Customers.ToList();

خط بالا دیگر یک عبارت پرس و جو نخواهد بود بلکه لیست تمام Customer هاست که به یکباره از database بازگشت داده شده است. در ادامه هرجا که از customers استفاده کنیم دیگر پرس و جویی به database فرستاده نخواهد شد.

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

private static void Query3()
{
    using (var context = new StoreDbContext())
    {
        var methodSyntaxquery = context.Customers
                   .Where(c => c.Name == "Mohsen");
        var sqlSyntaxquery = from c in context.Customers
                             where c.Name == "Mohsen"
                             select c;

        foreach (var customer in methodSyntaxquery)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                      customer.Name, customer.Family);
        }
    }

    // Output:
    // Customer Name: Mohsen, Customer Family: Jamshidi
    // Customer Name: Mohsen, Customer Family: Akbari
}

همانطور که مشاهده می‌کنید پرس و جو به دو روش Method Syntax و Sql Syntax نوشته شده است.

روش Method Syntax روشی است که از متدهای الحاقی (Extention Method)  ‌و عبارت‌های لامبدا (Lambda Expersion) برای نوشتن پرس و جو استفاده می‌شود. اما #C روش Sql Syntax را که همانند دستورات SQL هست، نیز فراهم کرده است تا کسانیکه آشنایی با این روش دارند، از این روش استفاده کنند. در نهایت این روش به Method Syntax تبدیل خواهد شد بنابراین پیشنهاد می‌شود که از همین روش استفاده شود تا با دست و پنجه نرم کردن با این روش، از مزایای آن در بخشهای دیگر کدنویسی استفاده شود.

اگر به نوع Customers که در DbContext تعریف شده است، دقت کرده باشید، خواهید دید که DbSet می‌باشد. DbSet کلاس و اینترفیس‌های متفاوتی را پیاده سازی کرده است که در ادامه با آنها آشنا خواهیم شد:

  • IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable: که امکان استفاده از متدهای نام آشنای LINQ را برای ما فراهم می‌کند. البته فراموش نشود که EF از Provider ای با نام LINQ To Entity برای تفسیر پرس و جوی ما و ساخت دستور SQL متناظر آن استفاده می‌کند. بنابراین تمامی متدهایی که در LINQ To Object استفاده می‌شوند در اینجا قابل استفاده نیستند. بطور مثال اگر در پرس و جو از LastOrDefault روی Customer استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود. 
LINQ to Entities does not recognize the method 'Store.Model.Customer LastOrDefault[Customer](System.Linq.IQueryable`1[Store.Model.Customer], System.Linq.Expressions.Expression`1[System.Func`2[Store.Model.Customer,System.Boolean]])' method, and this method cannot be translated into a store expression.
  • <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local می‌باشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده می‌شوند که  در ادامه توضیح داده خواهند شد.
  • <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include می‌باشد و در ادامه توضیح داده خواهند شد.

متد Find: این متد کلید اصلی را به عنوان ورودی گرفته و برای بازگرداندن نتیجه مراحل زیر را طی می‌کند:
  1. داده‌های موجود در حافظه را بررسی می‌کند یعنی آنهایی که Load و یا Attach شده اند.
  2. داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
  3. داده هایی که در database هستند ولی هنوز Load نشده اند.
Find در صورت پیدا نکردن Exception ای صادر نمی‌کند بلکه مقدار null را بر می‌گرداند.
private static void Query4()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.Find(new Guid("2ee2fd32-e0e9-4955-bace-1995839d4367"));

        if (customer == null)
            Console.WriteLine("Customer not found");
        else
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
    }
}
با توجه به اینکه Id‌ها توسط Database ساخته می‌شوند. شما باید از Id دیگری که موجود می‌باشد، استفاده کنید تا نتیجه ای برگشت داده شود.
نکته: در صورتیکه کلید اصلی شما از دو یا چند فیلد تشکیل شده بود. می‌بایست این دو یا چند مقدار را به عنوان پارامتر به Find بفرستید.

متد Single: گاهی نیاز هست که داده‌ای پرس و جو شود اما نه با کلید اصلی بلکه با شرط دیگری، در این حالت از Single استفاده می‌شود. این متد یک مقدار را باز می‌گرداند و در صورتی که صفر یا بیش از یک مقدار در شرط صدق کند exception صادر می‌کند. متد SingleOrDefault رفتاری مشابه دارد اما اگر مقداری در شرط صدق نکند مقدار پیش فرض را باز می‌گرداند.
نکته: مقدار پیش فرض بستگی به نوع خروجی دارد که اگر object باشد مقدار null و اگر بطور مثال نوع عددی باشد، صفر می‌باشد.
private static void Query5()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.Single(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        try
        {
            var customer2 = context.Customers.Single(c => c.Name == "Mohsen");  // Exception: Sequence contains more than one element
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer3 = context.Customers.SingleOrDefault(c => c.Name == "Unkown");  // customer3 == null

        var customer4 = context.Customers.Single(c => c.Name == "Vahid");  // customer4 != null
    }
}

متد First:
در صورتیکه به اولین نتیجه پرس و جو نیاز هست می‌توان از First استفاده کرد. اگر پرس و جو نتیجه در بر نداشته باشد یعنی null باشد exception صادر خواهد شد اما اگر FirstOrDefault استفاده شود مقدار پیش فرض برگردانده خواهد شد.
private static void Query6()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.First(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer2 = context.Customers.FirstOrDefault(c => c.Name == "Unknown");  // customer2 == null
        var customer3 = context.Customers.First(c => c.Name == "Mohsen");
    }
}

مطالب
روش یافتن لیست تمام کنترلرها و اکشن‌ متدهای یک برنامه‌ی ASP.NET Core
یک نمونه روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامه‌ی ASP.NET MVC 5.x را در مطلب «نحوه ایجاد یک نقشه‌ی سایت پویا با استفاده از قابلیت Reflection» می‌توانید ملاحظه کنید. استفاده‌ی از این روش با ASP.NET Core الزاما به پاسخ مناسبی نخواهد رسید؛ چون در اینجا POCO controllers هم اضافه شده‌اند. به علاوه می‌توان اسمبلی‌های دیگری را در زمان آغاز برنامه به تنظیمات AddMvc اضافه کرد و تمام آن‌ها هم می‌توانند حاوی کنترلرها و ویووها خاص خودشان باشند. روش بهتر این است که از خود ASP.NET Core سؤال کنیم چه مواردی را به عنوان کنترلر تشخیص داده‌ای؟ در ادامه این نکته را بیشتر بررسی خواهیم کرد.


معرفی سرویس IActionDescriptorCollectionProvider در ASP.NET Core

فرض کنید می‌خواهیم لیست تمام کنترلرهای یک برنامه‌ی ASP.NET Core را با ساختار ذیل تهیه کنیم که شامل نام کنترلر، نام اکشن متد و نام ناحیه‌ی متناظر با آن (در صورت تنظیم) می‌باشد:
public class MvcActionViewModel
{
    public string ControllerName { get; set; }
 
    public string ActionName { get; set; }
 
    public string AreaName { get; set; } 
}
یکی از سرویس‌های از پیش ثبت شده‌ی ASP.NET Core که لیست تمام کنترلرها و اکشن متدهای تشخیص داده شده‌ی توسط آن را به همراه دارد، سرویس IActionDescriptorCollectionProvider می‌باشد. برای شروع به کار با آن، ابتدا این سرویس را به سازنده‌ی یک کلاس دلخواه تزریق می‌کنیم:
public interface IMvcActionsDiscoveryService
{
    ICollection<MvcActionViewModel> MvcActions { get; }
}
 
public class MvcActionsDiscoveryService : IMvcActionsDiscoveryService
{
    public MvcActionsDiscoveryService(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
    {
        var actionDescriptors = actionDescriptorCollectionProvider.ActionDescriptors.Items;
        foreach (var actionDescriptor in actionDescriptors)
        {
            var descriptor = actionDescriptor as ControllerActionDescriptor;
            if (descriptor == null)
            {
                continue;
            }
 
            var controllerTypeInfo = descriptor.ControllerTypeInfo;
            var actionMethodInfo = descriptor.MethodInfo;
            MvcActions.Add(new MvcActionViewModel
            {
                ControllerName = descriptor.ControllerName,
                ActionName = descriptor.ActionName,
                AreaName = controllerTypeInfo.GetCustomAttribute<AreaAttribute>()?.RouteValue
            });
        }
    }
 
    public ICollection<MvcActionViewModel> MvcActions { get; } = new HashSet<MvcActionViewModel>(); 
}
توضیحات:
- در کلاس آغازین برنامه نیازی به ثبت سرویس IActionDescriptorCollectionProvider نیست و اینکار پیشتر توسط خود ASP.NET Core انجام شده‌است.
- این provider حاوی لیست اطلاعات تمام اکشن متدهای ثبت شده‌ی توسط ASP.NET Core است. در اینجا تنها کافی است حلقه‌ای را بر روی لیست آیتم‌های آن تشکیل داده و سپس مقادیر ControllerName و یا ActionName را بدست بیاوریم.
- اگر نیاز به اطلاعات بیشتری از کنترلر و اکشن متد جاری در حال بررسی توسط حلقه‌ی تهیه شده بود، می‌توان از ControllerTypeInfo و MethodInfo آن استفاده کرد. این TypeInfoها با استفاده از Reflection، امکان دسترسی به اطلاعاتی مانند ویژگی‌های اعمال شده‌ی به کنترلر یا اکشنی خاص را میسر می‌کنند. برای مثال در اینجا توسط اطلاعات نوع یک کنترلر در حال بررسی توانسته‌ایم متد GetCustomAttribute را فراخوانی کرده و سپس بررسی کنیم که آیا دارای ویژگی جدید Area هست یا خیر؟ و اگر بله، مقدار RouteValue آن را که در حقیقت مقدار یا نام Area آن کنترلر است، بازگشت می‌دهیم.


نحوه‌ی استفاده از سرویس IMvcActionsDiscoveryService تهیه شده

اگر دقت کرده باشید اطلاعات لیست MvcActions، در سازنده‌ی این کلاس مقدار دهی شده‌اند. علت اینجا است که اگر این کلاس را به صورت singleton ثبت کنیم، تنها یکبار در طول عمر برنامه و در همان آغاز کار، این لیست پر شده و سپس کش خواهد شد. بنابراین دسترسی‌های بعدی به MvcActions، شامل فراخوانی سازنده‌ی این کلاس نخواهند بود:
public static class MvcActionsDiscoveryServiceExtensions
{
    public static IServiceCollection AddMvcActionsDiscoveryService(this IServiceCollection services)
    {
        services.AddSingleton<IMvcActionsDiscoveryService, MvcActionsDiscoveryService>();
        return services;
    }
}
پس از تعریف متد الحاقی کمکی فوق برای افزودن سرویس تهیه شده به صورت singleton، برای ثبت آن در برنامه و در کلاس آغازین آن، خواهیم داشت:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvcActionsDiscoveryService();
}
در ادامه هر کنترلری و یا سرویس دیگری که نیاز به اطلاعات تمامی اکشن متدهای برنامه داشت، می‌تواند سرویس IMvcActionsDiscoveryService را به سازنده‌ی خود تزریق کرده و سپس از اطلاعات لیست MvcActions استفاده کند. این کاملترین لیستی که می‌توان تهیه کرد؛ زیرا زیرساخت ASP.NET Core نیز از همین سرویس IActionDescriptorCollectionProvider استفاده می‌کند.
مطالب
ساخت یک گزارش ساز به کمک 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 نهایی تولیدی یکپارچه و یک دست به نظر می‌رسد و اصلا مشخص نخواهد بود که این فایل پیشتر یک فرم قابل پر کردن بوده است.

مطالب دوره‌ها
بررسی جزئیات تزریق وابستگی‌ها در قالب پروژه WPF Framework
در قالب طراحی شده، نه در کدهای Viewهای اضافه شده و نه در ViewModelها، اثری از کدهای مرتبط با تزریق وابستگی‌ها و یا حتی وهله سازی ViewModel مرتبط با یک View مشاهده نمی‌شود. در ادامه قصد داریم جزئیات پیاده سازی آن‌را مرور کنیم.

مدیریت خودکار وهله سازی ViewModelها

اگر به فایل MVVM\ViewModelFactory.cs قرار گرفته در پروژه Common مراجعه کنید، کدهای کلاسی که کار وهله سازی ViewModelها را انجام می‌دهد، مشاهده خواهید کرد:
using System.Windows;
using StructureMap;

namespace WpfFramework1999.Common.MVVM
{
    /// <summary>
    /// Stitches together a view and its view-model
    /// </summary>
    public class ViewModelFactory
    {
        private readonly FrameworkElement _control;

        /// <summary>
        /// سازنده کلاس تزریق وابستگی‌ها به ویوو مدل و وهله سازی آن
        /// </summary>
        /// <param name="control">وهله‌ای از شیءایی که باید کار تزریق وابستگی‌ها در آن انجام شود</param>
        public ViewModelFactory(FrameworkElement control)
        {
            _control = control;
        }

        /// <summary>
        /// وهله متناظر با ویوو مدل
        /// </summary>
        public IViewModel ViewModelInstance { get; private set; }

        /// <summary>
        /// کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        /// </summary>        
        public void WireUp()
        {
            var viewName = _control.GetType().Name;
            var viewModelName = string.Concat(viewName, "ViewModel"); //قرار داد نامگذاری ما است

            if (!_control.IsLoaded)
            {
                _control.Loaded += (s, e) =>
                {
                    setDataContext(viewModelName);
                };
            }
            else
            {
                setDataContext(viewModelName);
            }
        }

        private void setDataContext(string viewModelName)
        {
            //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
            ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);
            if (ViewModelInstance == null) // این صفحه ویوو مدل ندارد
                return;

            _control.DataContext = ViewModelInstance;
        }
    }
}
در این کلاس، یک وهله از صفحه‌ای که توسط کاربر درخواست شده‌است، در سازنده کلاس دریافت گردیده و سپس در متد WireUp، بر اساس قرارداد نامگذاری که پیشتر نیز عنوان شد، ViewModel متناظر با نام View از IoC Container استخراج و وهله سازی می‌گردد. سپس این وهله به DataContext صفحه انتساب داده می‌شود.
چند سؤال مهم:
- IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟
- این کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟


IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟

اگر بحث سری جاری را از ابتدا دنبال کرده باشید، عنوان شد که ViewModelها را در این قالب، باید مشتق شده از کلاس پایه‌ای به نام BaseViewModel تهیه کنیم. برای مثال:
/// <summary>
/// ویوو مدل افزودن و مدیریت کاربران
/// </summary>
public class AddNewUserViewModel : BaseViewModel
این کلاس پایه که در فایل MVVM\BaseViewModel.cs پروژه Common قرار دارد، به نحو زیر آغاز شده است:
/// <summary>
/// کلاس پایه ویوو مدل‌های برنامه که جهت علامتگذاری آن‌ها برای سیم کشی‌های تزریق وابستگی‌های برنامه نیز استفاده می‌شود
/// </summary>
public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
اگر دقت کنید در اینجا اینترفیس IViewModel نیز ذکر شده است. این اینترفیس برای علامتگذاری ViewModelها و یافتن خودکار آن‌ها توسط IoC Container مورد استفاده درنظر گرفته شده است. اگر به فایل Core\IocConfig.cs پروژه Infrastructure مراجعه کنید، چنین تنظیمی را در آن مشاهده خواهید نمود:
// Add all types that implement IView into the container,
// and name each specific type by the short type name.
scan.AddAllTypesOf<IViewModel>().NameBy(type => type.Name);
به این ترتیب StructureMap با اسکن اسمبلی Infrastructure کلیه کلاس‌های پیاده سازی کننده IViewModel را یافته و سپس آن‌ها را بر اساس نام متناظری که دارند، ذخیره می‌کند. با این تنظیم، اکنون در کلاس ViewModelFactory یک چنین کدی کار خواهد کرد:
 //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);


کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟

در اینجا قسمتی از کدهای فایل Core\FrameFactory.cs قرار گرفته در پروژه Infrastructure را ملاحظه می‌کنید:
namespace WpfFramework.Infrastructure.Core
{
    /// <summary>
    /// ایجاد یک کنترل فریم سفارشی که قابلیت تزریق وابستگی‌ها را به صورت خودکار دارد
    /// به همراه اعمال مسایل راهبری برنامه که از منوی اصلی دریافت می‌شوند
    /// </summary>
    public class FrameFactory : Frame
    {
        /// <summary>
        /// در اینجا می‌شود به وهله‌ای از صفحه‌ای که قرار است اضافه گردد دسترسی یافت
        /// </summary>
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            var newPage = newContent as FrameworkElement;
            if (newPage == null)
                return;

            _currentViewModelFactory = new ViewModelFactory(newPage);
            _currentViewModelFactory.WireUp(); //کار تزریق وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        }
    }
}
در این کلاس، یک Frame سفارشی را طراحی کرده‌ایم؛ از این جهت که بتوان متد OnContentChanged آن‌را تحریف کرد. در این متد، newContent دقیقا وهله‌ای از صفحه جدیدی است که توسط کاربر درخواست شده‌است. خوب ... این وهله را داریم، بنابراین تنها کافی است آن‌را به کلاس ViewModelFactory ارسال کنیم و متد WireUp آن‌را بر روی وهله کلاس صفحه درخواستی فراخوانی نمائیم. به این ترتیب، صفحه‌ای نمایش داده خواهد شد که DataContext آن با وهله‌ای از ViewModel متناظر مقدار دهی شده‌است. از این جهت که این وهله سازی توسط IoC Container صورت می‌گیرد، کلیه وابستگی‌های تعریف شده در سازنده کلاس ViewModel نیز به صورت خودکار وهله سازی و مقدار دهی خواهند شد.

نهایتا فراخوانی متد IocConfig.Init، در فایل App.xaml.cs پروژه ریشه، در آغاز برنامه قرار گرفته است.
مطالب
بیلد سیستم گریدل Gradle Build System
امروزه در بسیاری از محیط‌های برنامه نویسی جاوا و اندروید، استفاده از این سیستم رایج است. ولی هنوز دیده می‌شود عده‌ای نسبت به آن دید روشنی ندارند و برای آن‌ها ناشناخته است و در حد یک سیستم کانفیگ آن را می‌شناسند. در این مقاله قصد داریم که مفهوم روشن‌تری از این سیستم را داشته باشم و بدانیم هدف آن چیست و چگونه کار می‌کند تا از این به بعد دیگر آن را به چشم یک کانفیگ کننده‌ی ساده نگاه نکنیم.  قبل از هر چیزی بهتر است که با تعدادی از اصطلاحات آن آشنا شویم تا در متن مقاله به مشکل برنخوریم:

DSL یا Domain Specific languages به معنی زبان‌هایی با دامنه محدود است که برای اهداف خاصی نوشته می‌شوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبان‌ها به شما اجازه نمی‌دهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه می‌دهند به هدفی که برای آن نوشته شده‌اند، برسید. یکی از این زبان‌ها همان  css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المان‌های وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدوده‌ای مشخص برای خود دارد. به این نوع از زبان‌های DSL، نوع اکسترنال هم می‌گویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interface‌ها که به ما شیوه آسانی از دسترسی به جنبه‌های یک شیء را می‌دهد. برای آشنایی هر چه بیشتر با این زبان‌ها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه می‌شود.

Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.

از دیرباز سیستم‌های Ant و Maven وجود داشتند و کار آن‌ها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آن‌ها و افزودن ویژگی‌های قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگی‌های گریدل می‌توان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستم‌ها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگین‌ها داشت و گوگل با استفاده از این قابلیت،  پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگین‌ها اجرا می‌شوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و می‌توان آن را در پروژه‌های جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی می‌کنند.

گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG  استفاده می‌کند و طبق آن ترتیب وظایف یا task‌ها را دانسته و آن‌ها را اجرا می‌کند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام می‌دهد.
  • در مرحله آماده سازی ما به گریدل می‌گوییم چه پروژه یا پروژه‌هایی نیاز به بیلد شدن دارند. در اندروید استادیو،  این مرحله در فایل settings.gradle انجام می‌شود؛ شما در این فایل مشخص می‌کنید چه پروژه‌های نیاز به بیلد شدن توسط گریدل دارند. ساختار این فایل به این شکل است:
include ':ActiveAndroid-master', ':app', ':dbutilities'
در این فایل سه پروژه برای گریدل مشخص شده‌اند. البته از نگاه Intellij سه ماژول معرفی شده‌اند و این فایل برای یک پروژه اختیاری است. گریدل برای پیدا کردن این فایل، از الگوریتم‌های متفاوتی استفاده می‌کند.
  1. در اولین مرحله انتظار دارد که فایل settings در دایرکتوری جاری باشد و اگر آن را پیدا کرد آن را مورد استفاده قرار می‌دهد؛ در غیر اینصورت مرحله بعدی را آغاز می‌کند.
  2. در مرحله دوم،  در این دایرکتوری به دنبال دایرکتوری به نام master میگردد و اگر در آن هم یافت نکرد مرحله سوم را آغاز می‌کند.
  3. در مرحله سوم، جست و جو در دایرکتوری والد انجام می‌شود
  4. چنانچه این فایل را در هیچ یک از احتمالات بالا نیابد، همین پروژه جاری را تشخیص خواهد داد.
اسامی پروژه‌های بالا با این تفکر هستند که در دایرکتوری یا فضای کاری جاری قرار گرفته‌اند که به آن IncludeFlat می‌گویند. ولی چنانچه آدرس پروژه‌ای در وضعیت خاص قرار بگیرد، می‌توان اینگونه مسیر آن را تغییر داد:
include ':ActiveAndroid-master', ':app', ':dbutilities'
project('dbutilities').projectDir=new File(settingsDir,'../dir1/dir2');
الان پروژه dbutiilities در سطح بالاتری از دایرکتوری جاری قرار گرفته است.

  • در مرحله پیکربندی، وظایف یا task‌ها را معرفی می‌کنیم. این عمل پیکربندی توسط فایل build.gradle که برای پروژه اصلی و هر زیر پروژه‌ای که مشخص شده‌اند، صورت می‌گیرد. در این فایل شما می‌توانید خواص و متدهایی را تعریف و و ظایفی را مشخص کنید.
    در پروژه اصلی، فایل BuildGradle شامل خطوط زیر است:
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
    }
}

allprojects {
    repositories {
        jcenter()

    }
}
در کلوژر buildscript مخازنی را که کتابخانه‌های نامبرده (وابستگی ها) در این کلوژ قرار میگیرند، معرفی میکنیم. در کلوژر بعدی تنظمیاتی را که برای همه پروژه‌ها اعمال می‌شوند، انجام می‌دهیم که در آن به معرفی مخزن jcenter پرداختیم. کتابخانه‌ای که در کلوژر buildscript صدا زدیم،  همان پلاگینی است که گوگل برای گریدل منتشر کرده که ما به آن ابزار بیلد می‌گوییم. حال به سراغ دیگر فایل‌های منحصر به فرد هر پروژه بروید. در این فایل‌ها،  شما تنظیمات پیش‌فرضی را می‌بینید که یکی از آن‌ها نسخه بندی پروژه است. پروژه‌های وابسته‌ای را که از مخازن باید دریافت شوند، شامل می‌شود و بسیاری از گزینه‌های دیگری که برای شما مهیا شده است تا در فاز پیکربندی، وظایف را بسازید.

در مرحله اجرا هم این وظایف را اجرا میکنیم.  تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper می‌باشد انجام می‌شود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، می‌توانید در ادامه دستور پیام‌های مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
task mytask <<{
    println ".net tips task in config phase"
}
در ابتدا عبارت task را به عنوان معرفی task می‌آوریم و سپس نام آن را وارد می‌کنیم. بعد از آن ما از عبارت‌های شیفت چپ>> استفاده کردیم. این عبارت شیفت چپ به این معناست که تسک مربوطه را آخر از همه وظایف اجرا کن که این عمل از طریق اجرای متدی به نام doLast صورت میگیرد. اگر در ترمینال اندروید عبارت زیر را تایپ کنید، متن مورد نظر باید نمایش یابد:
gradlew mytask
برای نمایش اطلاعات بیشتر می‌توانید از فلگ info هم استفاده کنید:
gradlew --info mytask
حال شاید بگویید من در بعضی از سایت‌ها یا مستندات و یا پروژه‌های دیگر دیده‌ام که عبارت >> را قرار نمی‌دهند. در این مورد باید گفت که آن‌ها در واقع تسک‌های اجرایی نیستند و برای پیکربندی به کار می‌روند و در فاز پیکربندی هم اجرا می‌شوند که در ادامه نمونه آن را خواهیم دید.
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، می‌توانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیام‌ها در کدام بخش چاپ می‌شوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ می‌شوند.
task mytask {
    println ".net tips task in config phase"

    doLast{
        println ".net tips task in exe phase"
    }
}

یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1،  تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسک‌ها  یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگی‌ها تنها به تسک‌های داخل فایل گریدل انجام می‌شود و نه تسک‌های پلاگین‌ها یا وابستگی هایی که تعریف می‌کنیم.
task mytask1 << {
    println ".net tips is the best"
}

task mytask2() {
    dependsOn mytask1

    doLast{
        Date time=Calendar.getInstance().getTime();
        SimpleDateFormat formatter=new SimpleDateFormat("HH:mm:ss , YYYY/MM/dd");
        println "mytask1 is done at " + formatter.format(time);

    }
}
سپس تسک شماره دو را صدا می‌زنیم. در اینجا جون تسک شماره دو به تسک شماره یک وابسته است، ابتدا تسک شماره یک اجرا شده و سپس نوبت تسک شماره دو می‌شود.
gradlew --info mytask2
خروجی کار:
Executing task ':app:mytask1' (up-to-date check took 0.003 secs) due to:
  Task has not declared any outputs.
خروجی تسک شماره یک
.net tips is the best       
:app:mytask1 (Thread[main,5,main]) completed. Took 0.046 secs.
:app:mytask2 (Thread[main,5,main]) started.
:app:mytask2                 
Executing task ':app:mytask2' (up-to-date check took 0.0 secs) due to:
  Task has not declared any outputs.
خروجی تسک شماره دو
mytask1 is done at 04:03:09 , 2016/07/07
:app:mytask2 (Thread[main,5,main]) completed. Took 0.075 secs.
               
BUILD SUCCESSFUL

در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسک‌های آماده ای نیز دارد که می‌توانید در مستندات آن بیابید. به عنوان مثال یکی از تسک‌های مفید و کاربردی آن تسک کپی کردن هست که از طریق آن می‌توانید فایلی یا فایل‌هایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسک‌های خود را به شکل زیر به شیوه اکشن بنویسید:
task mytask(type:Copy) {
    dependsOn mytask1

    doLast{
        from('build/apk')
                {
                    include '**/*.apk'
                }
        into '.'
    }

}
در تسک بالا بعد از اجرای تسک شماره یک، آخرین کاری که انجام می‌شود این است که فایل‌های apk موجود در زیر دایرکتوری‌های مسیر from به ریشه اصلی کپی خواهند شد. پس همانطور که می‌بینید گریدل به راحتی عملیات اتوماسیون را انجام می‌دهد.
برای نمایش تسک‌های موجود می‌توانید از گریدل درخواست کنید که لیست تمامی تسک‌های موجود را به شما نشان دهد. برای اینکار می‌توانید دستور زیر را صدا کنید:
gradlew --info tasks
بعد از مدتی تمامی تسک‌های موجود به صورت گروه بندی نمایش داده شده و تسک‌هایی که شما جدیدا اضافه کردید را با عنوان  other tasks نمایش خواهد داد:
Other tasks           
-----------           
clean                 
jarDebugClasses       
jarReleaseClasses     
mytask                
mytask2               
transformResourcesWithMergeJavaResForDebugUnitTest
transformResourcesWithMergeJavaResForReleaseUnitTest
اگر به تسک‌های خود گریدل نگاه کنید برای هر کدام توضیحی هم وجود دارد؛ اگر شما هم قصد دارید توضیحی اضافه کنید از خصوصیت description استفاده کنید:
task mytask(type:Copy) {

    description "copy apk files to root directory"

    dependsOn mytask1

    doLast{
        from('build/apk')
                {
                    include '**/*.apk'
                }
        into '.'
    }

}
یکبار دیگر دستور نمایش تسک‌ها را صدا بزنید تا اینبار توضیح اضافه شده نمایش داده شود.
یکی دیگر از نکات جالب در مورد گریدل این است که می‌تواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کرده‌ایم که به ما این اجازه را می‌دهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه می‌شود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ می‌کند.
tasks.whenTaskAdded{
    task-> println "task is added $task.name"
}

گریدل امکانات دیگری چون بررسی استثناءها و ایجاد استثناءها را هم پوشش می‌دهد که می‌توانید در این صفحه آن را پیگیری کنید.

Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژه‌ای که روی سیستم شما قرار بگیرد از نسخه‌ای متفاوتی از گریدل استفاده کند، باعث می‌شود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخه‌ای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا می‌زنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده می‌شود و wrapper به خوبی میداند که به چه نسخه‌ای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم می‌کند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید می‌توانید این دو فایل را ببینید.

از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژه‌ای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
gradle wrapper --gradle-version 2.4
دستور بالا یک wrapper برای نسخه 2.4 ایجاد می‌کند.
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
\gradle\wrapper\gradle-wrapper.properties
هنگامی که گوگل قصد آپدیت نسخه گریدل شما را بکند این فایل را باز کرده و نسخه داخل آن را ویرایش میکند.

این‌ها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشن‌تر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.
مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1#
قصد داریم در طی چند پست متوالی، یک پروژه Paint را به صورت شی گرا پیاده سازی کنیم. خوب، پروژه ای که می‌خواهیم پیاده سازی کنیم باید دارای این امکانات باشه که مرحله به مرحله پیش میریم و پروزه کامل در نهایت در قسمت پروژه‌ها ی همین سایت قرار خواهد گرفت.

  1. قابلیت ترسیم اشیا روی بوم گرافیکی دلخواه
  2. قابلیت جابجایی اشیا
  3. قابلیت تغییر رنگ اشیا
  4. ترسیم اشیا توپر و تو خالی
  5. تعیین پهنای خط شی ترسیم شده
  6. تعیین رنگ پس زمینه در صورت تو پر بودن شی
  7. قابلیت پیش نمایش رسم شکل در زمان ترسیم اشیا
  8. توانایی انتخاب اشیا
  9. تعیین عمق شی روی بوم گرافیکی مورد نظر
  10. ترسیم اشیایی مانند خط، دایره، بیضی، مربع، مستطیل، لوزی، مثلث 
  11. قابلیت تغییر اندازه اشیا ترسیم شده

خوب برای شروع ابتدا یک پروژه از نوع Windows Application ایجاد می‌کنیم (البته برای این قسمت می‌توانیم یک پروژه Class Library ایجاد کنیم)

سپس یک پوشه به نام Models به پروزه اضافه نمایید.

خوب در این پروژه یک کلاس پایه به نام Shape در نظر می‌گیریم.

همه اشیا ما دارای نقطه شروع، نقطه پایان، رنگ قلم، حالت انتخاب، رنگ پس زمینه، نوع شی، .... می‌باشند که بعضی از خصوصیات را توسط خصوصیات دیگر محاسبه می‌کنیم. مثلا خاصیت Width و Height و X و Y توسط خصوصیات نقطه شروع و پایان می‌توانند محاسبه شوند.

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

با توجه به تصویر بالا (البته این تجزیه تحلیل شخصی من بوده و دوستان به سلیقه خود ممکن است این ساختار را تغییر دهند)

نوع شمارشی ShapeType: در این نوع شمارشی انواع شی‌های موجود در پروژه معرفی شده است

محتوای این نوع به صورت زیر تعریف شده است:

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Shape Type in Paint
    /// </summary>
    public enum ShapeType
    {
        /// <summary>
        /// هیچ
        /// </summary>
        None = 0,
        /// <summary>
        /// خط
        /// </summary>
        Line = 1,
        /// <summary>
        /// مربع
        /// </summary>
        Square = 2,
        /// <summary>
        /// مستطیل
        /// </summary>
        Rectangle = 3,
        /// <summary>
        /// بیضی
        /// </summary>
        Ellipse = 4,
        /// <summary>
        /// دایره
        /// </summary>
        Circle = 5,
        /// <summary>
        /// لوزی
        /// </summary>
        Diamond = 6,
        /// <summary>
        /// مثلث
        /// </summary>
        Triangle = 7,
    }
}
انشاالله در پست‌های بعدی ما بقی کلاس‌ها به مرور پیاده سازی خواهند شد.

مطالب
ویژگی های کمتر استفاده شده در NET. - بخش هفتم

DebuggerStepThroughAttribute

ویژگی DebuggerStepThroughAttribute باعث می‌شود که در زمان دیباگ کردن کد، با کلید F11، متدهایی که این ویژگی را دارند، بدون رفتن به داخل متد (همانند دیباگ با کلید F10 عمل می‌کند، به جز زمانی که در داخل متد break point گذاشته باشید) ، تنها اجرا می‌شوند.
به مثال زیر توجه کنید:
class Program
{
    public static void Main(string[] args)
    {
        DebuggerStepThroughMethod1();
    }

    [DebuggerStepThrough]
    public static void DebuggerStepThroughMethod1()
    {
        Console.WriteLine( "Method 1" );
        DebuggerStepThroughMethod2();
    }

    [DebuggerStepThrough]
    public static void DebuggerStepThroughMethod2()
    {
        Console.WriteLine( "Method 2" );
    }
}
و نتیجه دیباگ با استفاده از F11 به صورت زیر می‌شود:

همانطور که مشاهده می‌کنید برنامه را با کلید F11 اجرا کردم. بعد از ورود به Method1، با زدن کلید F11 دستور بعدی، break point درون Method2 است.

ConditionalAttribute

شما با استفاده از Conditional می توانید اجرای یک متد را به شناساننده پیش پردازشی ( pre-processing identifier ) وابسته کنید. ConditionalAttribute می‌تواند بر روی یک کلاس یا یک متد بکار برده شود.
class Program
{
    public static void Main(string[] args)
    {
        DebugMode();
    }

    [Conditional("DEBUG")]
    public static void DebugMode()
    {
        Console.WriteLine( "Debug mode" );
    }
}
در صورتی که مثال بالا را در حالت Debug اجرا کنید، خروجی کنسول پیام Debug mode است و در صورتی که در حالت Release اجرا کنید، متد DebugMode اجرا نخواهد شد.
نکته: شما می‌توانید با استفاده از دستور define# (در بیرون از فضای نام) مقدار سفارشی خود را تعریف کنید.
#define ReleaseMode


Flags Enum Attribute

ویژگی Flags برای پوشش فیلدهای بیتی و انجام مقایسه بیتی استفاده می‌شود. از این ویژگی باید برای زمانیکه یک داده شمارشی می‌تواند چندین مقدار را به صورت همزمان داشته باشد، استفاده کرد.
[System.Flags]
public enum Permission
{
    View = 1,
    Insert = 2,
    Update = 4,
    Delete = 8
}
این نکته خیلی مهم است که Flags به صورت خودکار، مقادیر enum را به توان دو نمی‌رساند و شما باید به صورت دستی این مقادیر را تعیین کنید. در صورتیکه مقادیر عددی را تعیین نکنید، enum در عملیات بیتی به درستی کار نخواهد کرد، چرا که مقدار enum از 0 شروع می‌شود و افزایش پیدا می‌کند.  
public static void Main( string[] args )
{
    var permission = ( Permission.View | Permission.Insert ).ToString();
    Console.WriteLine( permission ); // Displays ‘View, Insert’

    var userPermission = Permission.View | Permission.Insert | Permission.Update | Permission.Delete;
    // To retrieve the value from property you can do this
    if ( ( userPermission & Permission.Delete ) == Permission.Delete )
    {
        Console.WriteLine( "کاربر دارای مجوز دسترسی به عملیات حذف می‌باشد" );
    }

    // In .NET 4 and later
    Console.WriteLine( userPermission.HasFlag( Permission.Delete )
                            ? "کاربر دارای مجوز دسترسی به عملیات حذف می‌باشد"
                            : "کاربر مجوز دسترسی به عملیات حذف را ندارد");
}

نکته: در صورتیکه مقداری را برای enum تعریف کرده باشید، نمی‌توانید آن را با مقدار 0 مشخص کنید (در زمانی که ویژگی flags را بر روی enum اضافه کرده باشید)، چرا که با استفاده از عملیات بیتی AND نمی‌توانید دارا بودن آن مقدار را تست کنید و همیشه نتیجه صفر خواهد بود.


Dynamically Compile and Execute C# Code

CodeDOM

با استفاده از CodeDOM می‌توانید یک سورس کد را به صورت یک فایل اسمبلی کامپایل و ذخیره کنید.
public static void Main( string[] args )
{
    var sourceCode = @"class DotNetTips
                        {
                            public void Print()
                            {
                                System.Console.WriteLine("".Net Tips"");
                            }
                        }";
    var compiledAssembly = CompileSourceCodeDom( sourceCode );
    ExecuteFromAssembly( compiledAssembly );
}

static Assembly CompileSourceCodeDom( string sourceCode )
{
    CodeDomProvider csharpCodeProvider = new CSharpCodeProvider();
    var cp = new CompilerParameters
                {
                    GenerateExecutable = false
                };
    cp.ReferencedAssemblies.Add( "System.dll" );
    var cr = csharpCodeProvider.CompileAssemblyFromSource( cp,
                                                            sourceCode );
    return cr.CompiledAssembly;
}
همانطور که در مثال بالا مشاهده می‌کنید، متغیر sourceCode حاوی کد مربوط به یک کلاس #C می‌باشد که یک متد Print در آن تعریف شده است.


Roslyn

سکوی کامپایلر دات نت " Roslyn "،  کامپایلرهای متن باز #C و  VB.NET را به همراه APIهای تجزیه و تحلیل کد ارائه کرده است که با استفاده از این APIها می‌توان ابزارهای آنالیز کد جهت استفاده در ویژوال استودیو را ایجاد کرد.

برای استفاده از Roslyn باید این کتابخانه را نصب کنید

Install-Package Microsoft.CodeAnalysis

حال مثال قبل را با استفاده از Roslyn بازنویسی می‌کنیم:

public static void Main(string[] args)
{
    var sourceCode = @"class DotNetTips
                        {
                            public void Print()
                            {
                                System.Console.WriteLine("".Net Tips"");
                            }
                        }";
    var compiledAssembly = CompileSourceRoslyn( sourceCode );
    ExecuteFromAssembly( compiledAssembly );
}

private static Assembly CompileSourceRoslyn(string sourceCode)
{
    using ( var memoryStream = new MemoryStream() )
    {
        var assemblyFileName = string.Concat( Guid.NewGuid().ToString(),
                                                ".dll" );
        var compilation = CSharpCompilation.Create( assemblyFileName,
                                                    new[]
                                                    {
                                                        CSharpSyntaxTree.ParseText( sourceCode )
                                                    },
                                                    new[]
                                                    {
                                                        MetadataReference.CreateFromFile( typeof( object ).Assembly.Location )
                                                    },
                                                    new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary ) );
        compilation.Emit( memoryStream );
        var assembly = Assembly.Load( memoryStream.GetBuffer() );
        return assembly;
    }
}

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

static void ExecuteFromAssembly( Assembly assembly )
{
    var helloKittyPrinterType = assembly.GetType( "DotNetTips" );
    var printMethod = helloKittyPrinterType.GetMethod( "Print" );
    var kitty = assembly.CreateInstance( "DotNetTips" );
    printMethod.Invoke( kitty,
                        BindingFlags.InvokeMethod,
                        null,
                        null,
                        CultureInfo.CurrentCulture );
}


مطالب دوره‌ها
ایجاد یک اسمبلی جدید توسط Reflection.Emit
مطابق استاندارد ECMA-335 قسمت دوم آن، یک اسمبلی از یک یا چند ماژول تشکیل می‌شود. هر ماژول از تعدادی نوع، enum و delegate تشکیل خواهد شد و هر نوع دارای تعدادی متد، فیلد، خاصیت و غیره می‌باشد. به همین جهت در حین کار با Reflection.Emit نیز این مراحل رعایت می‌شوند. ابتدا یک اسمبلی (AppDomain.DefineDynamicAssembly) ایجاد خواهد شد (یا از اسمبلی موجود استفاده می‌شود). سپس یک ماژول (AssemblyBuilder.DefineDynamicModule) را باید به آن اضافه کنیم (یا از ماژول اسمبلی جاری استفاده نمائیم). در ادامه یک Type باید به این ماژول اضافه شود (ModuleBuilder.DefineType) و اکنون می‌توان به این نوع جدید، سازنده (TypeBuilder.DefineConstructor)، متد (TypeBuilder.DefineMethod)، فیلد (TypeBuilder.DefineField)، خاصیت (TypeBuilder.DefineProperty) و رخداد (TypeBuilder.DefineEvent) اضافه کرد.


using System;
using System.Reflection;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    class Program
    {
        static void Main(string[] args)
        {
            var name = "HelloWorld.exe";
            var assemblyName = new AssemblyName(name);
            // ایجاد یک اسمبلی جدید با قابلیت ذخیره سازی آن
            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
                                            name: assemblyName,
                                            access: AssemblyBuilderAccess.RunAndSave);
            // افزودن یک ماژول به اسمبلی
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
            // تعریف یک کلاس در این ماژول
            var programmClass = moduleBuilder.DefineType("Program", TypeAttributes.Public);
            // افزودن یک متد به این کلاس
            // این متد خروجی ندارد اما ورودی آن شبیه به متد اصلی یک برنامه کنسول است
            var mainMethod = programmClass.DefineMethod(name: "Main",
                                            attributes: MethodAttributes.Public | MethodAttributes.Static,
                                            returnType: null,
                                            parameterTypes: new Type[] { typeof(string[]) });
            // تعیین بدنه متد اصلی برنامه
            var il = mainMethod.GetILGenerator();
            il.Emit(OpCodes.Ldstr, "Hello World!");
            il.Emit(OpCodes.Call, (typeof(Console)).GetMethod("WriteLine", new Type[] { typeof(string) }));
            il.Emit(OpCodes.Call, (typeof(Console)).GetMethod("ReadKey", new Type[0]));
            il.Emit(OpCodes.Pop);
            il.Emit(OpCodes.Ret);

            // تکمیل کار ایجاد نوع جدید
            programmClass.CreateType();

            // تعیین نقطه شروع فایل اجرایی برنامه کنسول تهیه شده
            assemblyBuilder.SetEntryPoint(((Type)programmClass).GetMethod("Main"));

            // ذخیره سازی این اسمبلی بر روی دیسک سخت
            assemblyBuilder.Save(name);
        }
    }
}
مراحلی را که توضیح داده شد، در کدهای فوق ملاحظه می‌کنید. انتخاب حالت دسترسی AssemblyBuilderAccess.RunAndSave سبب می‌شود تا بتوان نتیجه حاصل را ذخیره کرد. فایل Exe نهایی را اگر در برنامه ILSpy باز کنیم چنین شکلی دارد:
using System;
public class Program
{
    public static void Main(string[] array)
    {
       Console.WriteLine("Hello World!");
       Console.ReadKey();
    }
}
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت دوم - شروع به مستند سازی یک API
پس از معرفی اجمالی OpenAPI و Swagger در قسمت قبل و همچنین ارائه‌ی یک برنامه‌ی نمونه که آن‌را به مرور تکمیل خواهیم کرد، در ادامه کتابخانه‌ی Swashbuckle را نصب کرده و شروع به مستند سازی API ارائه شده خواهیم کرد.


نصب Swashbuckle (سوواَش باکِل)

اگر عبارت Swashbuckle.AspNetCore را در سایت NuGet جستجو کنیم، چندین بسته‌ی مختلف مرتبط با آن‌را خواهیم یافت. ما در این بین، بیشتر به این بسته‌ها علاقمندیم:
- Swashbuckle.AspNetCore.Swagger: کار آن ارائه‌ی خروجی OpenAPI تولیدی بر اساس ASP.NET Core API برنامه‌ی ما، به صورت یک JSON Endpoint است.
- Swashbuckle.AspNetCore.SwaggerGen: کار آن ساخت Swagger document objects است؛ یا همان OpenAPI Specification.
عموما این دو بسته را با هم جهت ارائه‌ی OpenAPI Specification استفاده می‌کنند.
- Swashbuckle.AspNetCore.SwaggerUI: این بسته، نگارش جایگذاری شده‌ی (embedded) ابزار swagger-UI را به همراه دارد. کار آن، ارائه‌ی یک UI خودکار، بر اساس OpenAPI Specification است که از آن برای آزمایش API نیز می‌توان استفاده کرد.

یک نکته: اگر صرفا بسته‌ی Swashbuckle.AspNetCore را نصب کنیم، هر سه بسته‌ی فوق را با هم دریافت خواهیم کرد و اگر از Visual Studio برای نصب آن‌ها استفاده می‌کنید، انتخاب گزینه‌ی Include prerelease را فراموش نکنید؛ از این جهت که قصد داریم از نگارش 5 آن‌ها استفاده کنیم. چون این نگارش است که از OpenAPI 3x، پشتیبانی می‌کند. خلاصه‌ی این موارد، افزودن PackageReference زیر به فایل پروژه‌ی OpenAPISwaggerDoc.Web.csproj است و سپس اجرای دستور dotnet restore:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc2" />
  </ItemGroup>
</Project>


تنظیم میان‌افزار Swashbuckle

پس از افزودن ارجاعی به Swashbuckle.AspNetCore، اکنون نوبت انجام تنظیمات میان‌افزارهای آن است. برای این منظور ابتدا به کلاس Startup و متد ConfigureServices آن مراجعه می‌کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
    // ...
            services.AddSwaggerGen(setupAction =>
            {
                setupAction.SwaggerDoc(
                   name: "LibraryOpenAPISpecification",
                   info: new Microsoft.OpenApi.Models.OpenApiInfo()
                   {
                       Title = "Library API",
                       Version = "1",
                       Description = "Through this API you can access authors and their books.",
                       Contact = new Microsoft.OpenApi.Models.OpenApiContact()
                       {
                           Email = "name@site.com",
                           Name = "DNT",
                           Url = new Uri("https://www.dntips.ir")
                       },
                       License = new Microsoft.OpenApi.Models.OpenApiLicense()
                       {
                           Name = "MIT License",
                           Url = new Uri("https://opensource.org/licenses/MIT")
                       }
                   });
            });
        }
در اینجا نحوه‌ی تنظیمات ابتدایی سرویس‌های مرتبط با SwaggerGen را ملاحظه می‌کنید. ابتدا نیاز است یک SwaggerDoc به آن اضافه شود که یک name و info را دریافت می‌کند. این name، جزئی از آدرسی است که در نهایت، OpenAPI Specification تولیدی را می‌توان در آنجا یافت. پارامتر Info آن نیز به همراه یک سری مشخصات عمومی درج شده‌ی در مستندات OpenAPI است.

اکنون در متد Configure، میان‌افزار آن‌را خواهیم افزود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            app.UseHttpsRedirection();

            app.UseSwagger();
    // ...
        }
بهتر است UseSwagger را پس از UseHttpsRedirection درج کرد تا هر نوع درخواست HTTP به آن، به صورت خودکار به HTTPS تبدیل و هدایت شود.

تا اینجا اگر برنامه را اجرا کنید، می‌توان OpenAPI Specification تولیدی را در آدرس زیر یافت:
 https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json


در این آدرس، LibraryOpenAPISpecification، همان نامی است که در قسمت setupAction.SwaggerDoc تنظیم کردیم.


نگاهی به OpenAPI Specification تولیدی

در ابتدای swagger.json تولیدی، همانطور که در تصویر فوق نیز مشخص است، همان مشخصات ذکر شده‌ی در قسمت info متد setupAction.SwaggerDoc، قابل مشاهده‌است. سپس لیست مسیرهای این API مشخص شده‌اند:


این‌ها مسیرهایی هستند که توسط دو کنترلر کتاب‌ها و نویسندگان برنامه‌ی Web API ما عمومی شده‌اند. در اینجا مقابل هر مسیر، تعداد آیتم‌های متناظری نیز ذکر شده‌اند. این موارد مرتبط هستند با HTTP methods پشتیبانی شده‌:


که هر کدام به همراه نام متدها و پارامترهای متناظر با آن‌ها نیز می‌شوند. به علاوه نوع responseهای پشتیبانی شده‌ی توسط این متدها نیز ذکر شده‌اند. هر کدام از خروجی‌ها نیز نوع مشخصی دارند که توسط قسمت components -> schemas تصاویر فوق، جزئیات دقیق آن‌ها بر اساس نوع مدل‌های متناظر، استخراج و ارائه شده‌اند.


مشکل: نوع Response تولیدی در OpenAPI Specification صحیح نیست


اگر به جزئیات مسیر /api/authors/{authorId} دقت کنیم، نوع response آن‌را صرفا 200 یا Ok ذکر کرده‌است؛ در حالیکه GetAuthor تعریف شده، حالت NotFound را نیز دارد:
[HttpGet("{authorId}")]
public async Task<ActionResult<Author>> GetAuthor(Guid authorId)
{
    var authorFromRepo = await _authorsService.GetAuthorAsync(authorId);
    if (authorFromRepo == null)
    {
        return NotFound();
    }
    return Ok(_mapper.Map<Author>(authorFromRepo));
}
نمونه‌ی دیگر آن اکشن متد public async Task<ActionResult<Book>> CreateBook است که می‌تواند NotFound یا 404 و یا CreatedAtRoute را که معادل 201 است، بازگشت دهد و در اینجا فقط 200 را ذکر کرده‌است که اشتباه است. بنابراین برای نزدیک کردن این خروجی به اطلاعات واقعی اکشن متدها، نیاز است کار بیشتری انجام شود.


افزودن و راه اندازی Swagger UI

در ادامه می‌خواهیم یک رابط کاربری خودکار را بر اساس OpenAPI Specification تولیدی، ایجاد کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...

            app.UseHttpsRedirection();

            app.UseSwagger();
            app.UseSwaggerUI(setupAction =>
            {
                setupAction.SwaggerEndpoint(
                    "/swagger/LibraryOpenAPISpecification/swagger.json",
                    "Library API");
            });

    // ...
        }
برای این منظور میان‌افزار SwaggerUI را پس از UseSwagger، در متد Configure کلاس Startup، تعریف می‌کنیم. در اینجا باید مشخص کنیم که OpenAPI Specification تولید شده، دقیقا در چه آدرسی قرار دارد که روش انجام آن‌را در متد setupAction.SwaggerEndpoint ملاحظه می‌کنید. پارامتر دوم آن یک نام اختیاری است.
پس از این تنظیم اگر آدرس https://localhost:5001/swagger/index.html را در مرورگر باز کنیم، چنین خروجی قابل مشاهده خواهد بود:


و اگر بر روی هر کدام کلیک کنیم، ریز جزئیات آن‌ها بر اساس OpenAPI Specification ای که بررسی کردیم، تولید شده‌است (از پارامترها تا نوع خروجی):


اکنون اگر بر روی دکمه‌ی try it out آن نیز کلیک کنید، در همینجا می‌توان این API را آزمایش کرد. برای مثال Controls Accept header را بر روی application/json قرار داده و سپس بر روی دکمه‌ی execute که پس از کلیک بر روی دکمه‌ی try it out ظاهر شده‌است، کلیک کنید تا بتوان خروجی Web API را مشاهده کرد.

در انتهای این صفحه، در قسمت schemas آن، مشخصات مدل‌های بازگشت داده شده‌ی توسط Web API نیز ذکر شده‌اند:



یک نکته: تغییر آدرس  https://localhost:5001/swagger/index.html به ریشه‌ی سایت

اگر علاقمند باشید تا زمانیکه برای اولین بار آدرس ریشه‌ی سایت را در مسیر https://localhost:5001 باز می‌کنید، Swagger UI نمایان شود، می‌توانید تنظیم RoutePrefix زیر را اضافه کنید:
app.UseSwaggerUI(setupAction =>
            {
                setupAction.SwaggerEndpoint(
                    "/swagger/LibraryOpenAPISpecification/swagger.json",
                    "Library API");
                setupAction.RoutePrefix = "";
            });


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-02.zip

در قسمت بعد، به بهبود و غنی سازی جزئیات OpenAPI Specification تولیدی خواهیم پرداخت.
مطالب
آموزش WAF (مشاهده تغییرات خواص ViewModel در Controller)
قصد داریم در مثال پست قبلی برای Command مورد نظر، عملیات اعتبارسنجی را فعال کنیم. اگر با الگوی MVVM آشنایی داشته باشید می‌دانید که می‌توان برای Command‌ها اکشنی به عنوان CanExecute تعریف کرد و در آن عملیات اعتبارسنجی را انجام داد. اما از آن جا که پیاده سازی این روش زمانی مسیر است که تغییرات خواص ViewModel در دسترس باشد در نتیجه در WAF مکانیزمی جهت ردیابی تغییرات خواص ViewModel در کنترلر فراهم شده است. در نسخه‌های قبلی WAF (قبل از نسخه 3) هر کنترلر از کلاس پایه ای به نام Controller ارث می‌برد که متد هایی جهت ردیابی تغییرات در آن در نظر گرفته شده بود به صورت زیر:
public class MyController : Controller
    {
        [ImportingConstructor]
        public MyController(MyViewModel viewModel)
        {
            ViewModelCore = viewModel;
        }

        public MyViewModel ViewModelCore 
        {
            get; 
            private set; 
        }

        public void Run()
        {
            AddWeakEventListener(ViewModelCore , ViewModelCoreChanged)
        }

        private void ViewModelCoreChanged(object sender , PropertyChangedEventArgs e)
        {
            if(e.PropertyName=="CurrentItem")
            {
                
            }
        }
    }
همان طور که مشاهده می‌کنید با استفاده از متد AddWeakEventListener توانستیم تمامی تغییرات خواص ViewModel مورد نظر را از طریق متد ViewModelCoreChanged ردیابی کنیم. این متد بر مبنای الگوی WeakEvent پیاده سازی شده است. البته این تغییرات فقط زمانی قابل ردیابی هستند که  در ViewModel متد RaisePropertyChanged برای متد set خاصیت فراخوانی شده باشد.
از آنجا که در دات نت 4.5 یک پیاده سازی خاص از الگوی WeakEvent در کلاس PropertyChangedEventManager موجود در اسمبلی WindowsBase و فضای نام System.ComponentModel انجام شده است در نتیجه توسعه دهندگان این کتابخانه نیز تصمیم به استفاده از این روش گرفتند که نتیجه آن  Obsolete شدن کلاس پایه کنترلر در نسخه‌های 3 به بعد آن است. در روش جدید کافیست به صورت زیر عمل نمایید:
 [Export]
    public class BookController
    {
        [ImportingConstructor]
        public BookController(BookViewModel viewModel)
        {
            ViewModelCore = viewModel;
        }
        
        public BookViewModel ViewModelCore
        {
            get;
            private set;
        }

        public DelegateCommand RemoveItemCommand 
        { 
            get; 
            private set;
        }

        private void ExecuteRemoveItemCommand()
        {
            ViewModelCore.Books.Remove(ViewModelCore.CurrentItem);
        }

        private bool CanExecuteRemoveItemCommand()
        {
            return ViewModelCore.CurrentItem != null;
        }
        private void Initialize()
        {
            RemoveItemCommand = new DelegateCommand(ExecuteRemoveItemCommand , CanExecuteRemoveItemCommand);
            ViewModelCore.RemoveItemCommand = RemoveItemCommand;
        }

        public void Run()
        {
            var result = new List<Book>();
            result.Add(new Book { Code = 1, Title = "Book1" });
            result.Add(new Book { Code = 2, Title = "Book2" });
            result.Add(new Book { Code = 3, Title = "Book3" });

            Initialize();
            ViewModelCore.Books = new ObservableCollection<Models.Book>(result);

            PropertyChangedEventManager.AddHandler(ViewModelCore, ViewModelChanged, "CurrentItem");
            
            (ViewModelCore.View as IBookView).Show();
        }

        private void ViewModelChanged(object sender,PropertyChangedEventArgs e)
        {
            if(e.PropertyName == "CurrentItem")
            {
                RemoveItemCommand.RaiseCanExecuteChanged();
            }
        }
    }
تغییرات:
»ابتدا متدی به نام CanExecuteRemoveItemCommand ایجاد کردیم و کد‌های اعتبارسنجی را در آن قرار دادیم؛
»هنگام تعریف Command مربوطه متد بالا را به DelegateCommand رجیستر کردیم:
  RemoveItemCommand = new DelegateCommand(ExecuteRemoveItemCommand , CanExecuteRemoveItemCommand);
در این حالت بعد از اجرای برنامه همواره دکمه RemoveItem غیر فعال خواهد بود. دلیل آن این است که بعد از انتخاب آیتم مورد نظر از لیست باید کنترلر را متوجه تغییر در مقدار خاصیت CurrentItem نماییم. بدین منظور کد زیر را به متد Run اضافه کردم:
     PropertyChangedEventManager.AddHandler(ViewModelCore, ViewModelChanged, "CurrentItem");
دستور بالا دقیقا معادل دستور AddWeakEventListener موجود در نسخه‌های قدیمی WAF است. سپس در صورتی که نام خاصیت مورد نظر CurrentItem بود با استفاده از دستور RaiseCanExecuteChanged در کلاس DelegateCommand کنترلر را ملزم به اجرای دوباره متد CanExecuteRemoveItemCommand می‌کنیم.
اجرای برنامه:
ابتدا دکمه RemoveItem غیر فعال است:

بعد از انتخاب یکی از گزینه و فراخوانی مجدد متد CanExecuteRemoveItemCommand دکمه مورد نظر فعال می‌شود:

و در نهایت دکمه RemoveItem فعال خواهد شد:


دانلود سورس پروژه