نظرات مطالب
فارسی نویسی و iTextSharp
نه. لازم است به ازای هر سلول اینکار انجام شود.
ضمنا یک نکته کلی در مورد PDF وجود دارد و آن هم این است که ساختار PDF یک canvas است (یک تابلو نقاشی برداری). یعنی مفاهیمی مانند جدول، سلول، پاراگراف و غیره در پشت صحنه آن وجود خارجی ندارند و فقط کتابخانه‌های تولید PDF است که این نوع امکانات را جهت سهولت کار اختراع کرده‌اند. بنابراین به ازای هر شیءایی که اضافه می‌شود باید اطلاعات دقیق آن نیز درج شود.

نظرات مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core
        public async Task<IActionResult> FileUpload(IFormFile file)
        {
            if (file == null || file.Length == 0)
            {
                return BadRequest();
            }

            using (var memoryStream = new MemoryStream())
            {
                await file.CopyToAsync(memoryStream);
                var fileBytes = memoryStream.ToArray();
                // ... save it
            }
        }
نظرات مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
var initialLoad = true;

var ds = new kendo.data.DataSource({
//...
requestStart: function () {
  if (initialLoad)
     kendo.ui.progress($("#myGrid"), true);
},
requestEnd: function () {
  if(initialLoad)
      kendo.ui.progress($("#myGrid"), false);
  initialLoad = false;
},
//...
نظرات مطالب
ASP.NET MVC #8
از یکی از دو روش زیر استفاده کنید:
var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var url = urlHelper.Action("Home", "Index");
و یا
UrlHelper.GenerateUrl(null, actionName, controllerName,
                               null, null, null, routeValues, htmlHelper.RouteCollection, 
                               htmlHelper.ViewContext.RequestContext, true);
مطالب
بررسی مقدمات کتابخانه‌ی JSON.NET
چرا JSON.NET؟
JSON.NET یک کتابخانه‌ی سورس باز کار با اشیاء JSON در دات نت است. تاریخچه‌ی آن به 8 سال قبل بر می‌گردد و توسط یک برنامه نویس نیوزیلندی به نام James Newton King تهیه شده‌است. اولین نگارش آن در سال 2006 ارائه شد؛ مقارن با زمانی که اولین استاندارد JSON نیز ارائه گردید.
این کتابخانه از آن زمان تا کنون، 6 میلیون بار دانلود شده‌است و به علت کیفیت بالای آن، این روزها پایه اصلی بسیاری از کتابخانه‌ها و فریم ورک‌های دات نتی می‌باشد؛ مانند RavenDB تا ASP.NET Web API و SignalR مایکروسافت و همچنین گوگل نیز از آن جهت تدارک کلاینت‌های کار با API خود استفاده می‌کنند.
هرچند دات نت برای نمونه در نگارش سوم آن جهت مصارف WCF کلاسی را به نام DataContractJsonSerializer ارائه کرد، اما کار کردن با آن محدود است به فرمت خاص WCF به همراه عدم انعطاف پذیری و سادگی کار با آن. به علاوه باید درنظر داشت که JSON.NET از دات نت 2 به بعد تا مونو، Win8 و ویندوز فون را نیز پشتیبانی می‌کند.

برای نصب آن نیز کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
 PM> install-package Newtonsoft.Json

معماری JSON.NET

کتابخانه‌ی JSON.NET از سه قسمت عمده تشکیل شده‌است:
الف) JsonSerializer
ب) LINQ to JSON
ج) JSON Schema


الف) JsonSerializer
کار JsonSerializer تبدیل اشیاء دات نتی به JSON و برعکس است. مزیت مهم آن امکانات قابل توجه تنظیم عملکرد و خروجی آن می‌باشد که این تنظیمات را به شکل ویژگی‌های خواص نیز می‌توان اعمال نمود. به علاوه امکان سفارشی سازی هر کدام نیز توسط کلاسی به نام JsonConverter، پیش بینی شده‌است.
یک مثال:
 var roles = new List<string>
{
   "Admin",
   "User"
};
string json = JsonConvert.SerializeObject(roles, Formatting.Indented);
در اینجا نحوه‌ی استفاده از JSON.NET را جهت تبدیل یک شیء دات نتی، به معادل JSON آن مشاهده می‌کنید. اعمال تنظیم Formatting.Indented سبب خواهد شد تا خروجی آن دارای Indentation باشد. برای نمونه اگر در برنامه‌ی خود قصد دارید فرمت JSON تو در تویی را به نحو زیبا و خوانایی نمایش دهید یا چاپ کنید، همین تنظیم ساده کافی خواهد بود.
و یا در مثال ذیل استفاده از یک anonymous object را مشاهده می‌کنید:
 var jsonString = JsonConvert.SerializeObject(new
{
   Id =1,
   Name = "Test"
}, Formatting.Indented);
به صورت پیش فرض تنها خواص عمومی کلاس‌ها توسط JSON.NET تبدیل خواهند شد.


تنظیمات پیشرفته‌تر JSON.NET

مزیت مهم JSON.NET بر سایر کتابخانه‌ها‌ی موجود مشابه، قابلیت‌های سفارشی سازی قابل توجه آن است. در مثال ذیل نحوه‌ی معرفی JsonSerializerSettings را مشاهده می‌نمائید:
var jsonData = JsonConvert.SerializeObject(new
{
   Id = 1,
   Name = "Test",
   DateTime = DateTime.Now
}, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   Converters =
   {
      new JavaScriptDateTimeConverter()
   }
});
در اینجا با استفاده از تنظیم JavaScriptDateTimeConverter، می‌توان خروجی DateTime استانداردی را به مصرف کنندگان جاوا اسکریپتی سمت کاربر ارائه داد؛ با خروجی ذیل:
 {
  "Id": 1,
  "Name": "Test",
  "DateTime": new Date(1409821985245)
}


نوشتن خروجی JSON در یک استریم

خروجی متد JsonConvert.SerializeObject یک رشته‌است که در صورت نیاز به سادگی توسط متد File.WriteAllText در یک فایل قابل ذخیره می‌باشد. اما برای رسیدن به حداکثر کارآیی و سرعت می‌توان از استریم‌ها نیز استفاده کرد:
using (var stream = File.CreateText(@"c:\output.json"))
{
    var jsonSerializer = new JsonSerializer
   {
      Formatting = Formatting.Indented
   };
   jsonSerializer.Serialize(stream, new
   {
     Id = 1,
     Name = "Test",
     DateTime = DateTime.Now
   });
}
کلاس JsonSerializer و متد Serialize آن یک استریم را نیز جهت نوشتن خروجی می‌پذیرند. برای مثال response.Output برنامه‌های وب نیز یک استریم است و در اینجا نوشتن مستقیم در استریم بسیار سریعتر است از تبدیل شیء به رشته و سپس ارائه خروجی آن؛ زیرا سربار تهیه رشته JSON از آن حذف می‌گردد و نهایتا GC کار کمتری را باید انجام دهد.


تبدیل JSON رشته‌ای به اشیاء دات نت

اگر رشته‌ی jsonData ایی را که پیشتر تولید کردیم، بخواهیم تبدیل به نمونه‌ای از شیء User ذیل کنیم:
public class User
{
   public int Id { set; get; }
   public string Name { set; get; }
   public DateTime DateTime { set; get; }
}
خواهیم داشت:
 var user = JsonConvert.DeserializeObject<User>(jsonData);
در اینجا از متد DeserializeObject به همراه مشخص سازی صریح نوع شیء نهایی استفاده شده‌است.
البته در اینجا با توجه به استفاده از JavaScriptDateTimeConverter برای تولید jsonData، نیاز است چنین تنظیمی را نیز در حالت DeserializeObject مشخص کنیم:
var user = JsonConvert.DeserializeObject<User>(jsonData, new JsonSerializerSettings
{
   Converters = {  new JavaScriptDateTimeConverter() }
});


مقدار دهی یک نمونه یا وهله‌ی از پیش موجود

متد JsonConvert.DeserializeObject یک شیء جدید را ایجاد می‌کند. اگر قصد دارید صرفا تعدادی از خواص یک وهله‌ی موجود، توسط JSON.NET مقدار دهی شوند از متد PopulateObject استفاده کنید:
 JsonConvert.PopulateObject(jsonData, user);


کاهش حجم JSON تولیدی

زمانیکه از متد JsonConvert.SerializeObject استفاده می‌کنیم، تمام خواص عمومی تبدیل به معادل JSON آن‌ها خواهند شد؛ حتی خواصی که مقدار ندارند. این خواص در خروجی JSON، با مقدار null مشخص می‌شوند. برای حذف این خواص از خروجی JSON نهایی تنها کافی است در تنظیمات JsonSerializerSettings، مقدار NullValueHandling = NullValueHandling.Ignore مشخص گردد.
var jsonData = JsonConvert.SerializeObject(object, new JsonSerializerSettings
{
   NullValueHandling = NullValueHandling.Ignore,
   Formatting = Formatting.Indented
});
مورد دیگری که سبب کاهش حجم خروجی نهایی خواهد شد، تنظیم DefaultValueHandling = DefaultValueHandling.Ignore است. در این حالت کلیه خواصی که دارای مقدار پیش فرض خودشان هستند، در خروجی JSON ظاهر نخواهند شد. مثلا مقدار پیش فرض خاصیت int مساوی صفر است. در این حالت کلیه خواص از نوع int که دارای مقدار صفر می‌باشند، در خروجی قرار نمی‌گیرند.
به علاوه حذف Formatting = Formatting.Indented نیز توصیه می‌گردد. در این حالت فشرده‌ترین خروجی ممکن حاصل خواهد شد.


مدیریت ارث بری توسط JSON.NET

در مثال ذیل کلاس کارمند و کلاس مدیر را که خود نیز در اصل یک کارمند می‌باشد، ملاحظه می‌کنید:
public class Employee
{
    public string Name { set; get; }
}

public class Manager : Employee
{
    public IList<Employee> Reports { set; get; }
}
در اینجا هر مدیر لیست کارمندانی را که به او گزارش می‌دهند نیز به همراه دارد. در ادامه نمونه‌ای از مقدار دهی این اشیاء ذکر شده‌اند:
 var employee = new Employee { Name = "User1" };
var manager1 = new Manager { Name = "User2" };
var manager2 = new Manager { Name = "User3" };
manager1.Reports = new[] { employee, manager2 };
manager2.Reports = new[] { employee };
با فراخوانی
 var list = JsonConvert.SerializeObject(manager1, Formatting.Indented);
یک چنین خروجی JSON ایی حاصل می‌شود:
{
  "Reports": [
    {
      "Name": "User1"
    },
    {
      "Reports": [
        {
          "Name": "User1"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
این خروجی JSON جهت تبدیل به نمونه‌ی معادل دات نتی خود، برای مثال جهت رسیدن به manager1 در کدهای فوق، چندین مشکل را به همراه دارد:
- در اینجا مشخص نیست که این اشیاء، کارمند هستند یا مدیر. برای مثال مشخص نیست User2 چه نوعی دارد و باید به کدام شیء نگاشت شود.
- مشکل دوم در مورد کاربر User1 است که در دو قسمت تکرار شده‌است. این شیء JSON اگر به نمونه‌ی معادل دات نتی خود نگاشت شود، به دو وهله از User1 خواهیم رسید و نه یک وهله‌ی اصلی که سبب تولید این خروجی JSON شده‌است.

برای حل این دو مشکل، تغییرات ذیل را می‌توان به JSON.NET اعمال کرد:
var list = JsonConvert.SerializeObject(manager1, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   TypeNameHandling = TypeNameHandling.Objects,
   PreserveReferencesHandling = PreserveReferencesHandling.Objects
});
با این خروجی:
{
  "$id": "1",
  "$type": "JsonNetTests.Manager, JsonNetTests",
  "Reports": [
    {
      "$id": "2",
      "$type": "JsonNetTests.Employee, JsonNetTests",
      "Name": "User1"
    },
    {
      "$id": "3",
      "$type": "JsonNetTests.Manager, JsonNetTests",
      "Reports": [
        {
          "$ref": "2"
        }
      ],
      "Name": "User3"
    }
  ],
  "Name": "User2"
}
- با تنظیم TypeNameHandling = TypeNameHandling.Objects سبب خواهیم شد تا خاصیت اضافه‌ای به نام $type به خروجی JSON اضافه شود. این نوع، در حین فراخوانی متد JsonConvert.DeserializeObject جهت تشخیص صحیح نگاشت اشیاء بکار گرفته خواهد شد و اینبار مشخص است که کدام شیء، کارمند است و کدامیک مدیر.
- با تنظیم PreserveReferencesHandling = PreserveReferencesHandling.Objects شماره Id خودکاری نیز به خروجی JSON اضافه می‌گردد. اینبار اگر به گزارش دهنده‌ها با دقت نگاه کنیم، مقدار $ref=2 را خواهیم دید. این مورد سبب می‌شود تا در حین نگاشت نهایی، دو وهله متفاوت از شیء با Id=2 تولید نشود.

باید دقت داشت که در حین استفاده از JsonConvert.DeserializeObject نیز باید JsonSerializerSettings یاد شده، تنظیم شوند.


ویژگی‌های قابل تنظیم در JSON.NET

علاوه بر JsonSerializerSettings که از آن صحبت شد، در JSON.NET امکان تنظیم یک سری از ویژگی‌ها به ازای خواص مختلف نیز وجود دارند.
- برای نمونه ویژگی JsonIgnore معروفترین آن‌ها است:
public class User
{
   public int Id { set; get; }

   [JsonIgnore]
   public string Name { set; get; }

   public DateTime DateTime { set; get; }
}
JsonIgnore سبب می‌شود تا خاصیتی در خروجی نهایی JSON تولیدی حضور نداشته باشد و از آن صرفنظر شود.

- با استفاده از ویژگی JsonProperty اغلب مواردی را که پیشتر بحث کردیم مانند NullValueHandling، TypeNameHandling و غیره، می‌توان تنظیم نمود. همچنین گاهی از اوقات کتابخانه‌های جاوا اسکریپتی سمت کاربر، از اسامی خاصی که از روش‌های نامگذاری دات نتی پیروی نمی‌کنند، در طراحی خود استفاده می‌کنند. در اینجا می‌توان نام خاصیت نهایی را که قرار است رندر شود نیز صریحا مشخص کرد. برای مثال:
[JsonProperty(PropertyName = "m_name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { set; get; }
همچنین در اینجا امکان تنظیم Order نیز وجود دارد. برای مثال مشخص کنیم که خاصیت X در ابتدا قرار گیرد و پس از آن خاصیت Y رندر شود.

- استفاده از ویژگی JsonObject به همراه مقدار OptIn آن به این معنا است که از کلیه خواصی که دارای ویژگی JsonProperty نیستند، صرفنظر شود. حالت پیش فرض آن OptOut است؛ یعنی تمام خواص عمومی در خروجی JSON حضور خواهند داشت منهای مواردی که با JsonIgnore مزین شوند.
[JsonObject(MemberSerialization.OptIn)]
public class User
{
    public int Id { set; get; }

    [JsonProperty]
    public string Name { set; get; }
 
    public DateTime DateTime { set; get; }
}

- با استفاده از ویژگی JsonConverter می‌توان نحوه‌ی رندر شدن مقدار خاصیت را سفارشی سازی کرد. برای مثال:
[JsonConverter(typeof(JavaScriptDateTimeConverter))]
public DateTime DateTime { set; get; }


تهیه یک JsonConverter سفارشی

با استفاده از JsonConverterها می‌توان کنترل کاملی را بر روی اعمال serialization و deserialization مقادیر خواص اعمال کرد. مثال زیر را در نظر بگیرید:
public class HtmlColor
{
   public int Red { set; get; }
   public int Green { set; get; }
   public int Blue { set; get; }
}

var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
}, Formatting.Indented);
در اینجا علاقمندیم، در حین عملیات serialization، بجای اینکه مقادیر اجزای رنگ تهیه شده به صورت int نمایش داده شوند، کل رنگ با فرمت hex رندر شوند. برای اینکار نیاز است یک JsonConverter سفارشی را تدارک دید:
    public class HtmlColorConverter : JsonConverter
    {

        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(HtmlColor);
        }

        public override object ReadJson(JsonReader reader, Type objectType,
                                        object existingValue, JsonSerializer serializer)
        {
            throw new NotSupportedException();
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var color = value as HtmlColor;
            if (color == null)
                return;

            writer.WriteValue("#" + color.Red.ToString("X2")
                + color.Green.ToString("X2") + color.Blue.ToString("X2"));
        }
    }
کار با ارث بری از کلاس پایه JsonConverter شروع می‌شود. سپس باید تعدادی از متدهای این کلاس پایه را بازنویسی کرد. در متد CanConvert اعلام می‌کنیم که تنها اشیایی از نوع کلاس HtmlColor را قرار است پردازش کنیم. سپس در متد WriteJson منطق سفارشی خود را می‌توان پیاده سازی کرد.
از آنجائیکه این تبدیلگر صرفا قرار است برای حالت serialization استفاده شود، قسمت ReadJson آن پیاده سازی نشده‌است.

در آخر برای استفاده از آن خواهیم داشت:
var colorJson = JsonConvert.SerializeObject(new HtmlColor
{
  Red = 255,
  Green = 0,
  Blue = 0
},  new JsonSerializerSettings
    {
      Formatting = Formatting.Indented,
      Converters = { new HtmlColorConverter() }
    });   
مطالب
نمایش ساختارهای درختی توسط jqGrid
jqGrid از نمایش دو ساختار درختی Nested Set model و Adjacency model پشتیبانی می‌کند. توضیحات تکمیلی و پایه‌ای را در مورد این دو روش مدل سازی اطلاعات، در مطلب «SQL Antipattern #2» می‌توانید مطالعه کنید.
در اینجا روش Adjacency model را به علت بیشتر مرسوم بودن آن و شباهت بسیار زیاد آن به «مدل‌های خود ارجاع دهنده» بررسی خواهیم کرد.


مدل داده‌ای Adjacency

در حالت ساختار درختی از نوع مجاورت، علاوه بر خواص اصلی یک کلاس، سه خاصیت دیگر نیز باید تعریف شوند:
using System;

namespace jqGrid13.Models
{
    public class BlogComment
    {
        // Other properties 
        public int Id { set; get; }
        public string Body { set; get; }
        public DateTime AddDateTime { set; get; }

        // for treeGridModel: 'adjacency'
        public int? ParentId { get; set; }
        public bool IsNotExpandable { get; set; }
        public bool IsExpanded { get; set; }
    }
}
ParentId که سبب تولید یک مدل خود ارجاع دهنده می‌شود.
IsNotExpandable به این معنا است که نود جاری آیا قرار است باز شود و فرزندی دارد یا خیر؟ اگر فرزندی ندارد باید مساوی True قرار گیرد.
IsExpanded حالت پیش فرض باز بودن یا نبودن یک نود را مشخص می‌کند.


نحوه‌ی بازگشت اطلاعات درختی از سمت سرور

در نگارش فعلی jqGrid، در حالت نمایش درختی، مباحث صفحه بندی و مرتب سازی غیرفعال هستند و کدهای مرتبط با آن که در اینجا ذکر شده‌اند، فعلا تاثیری ندارند (البته با کمی تغییر در کدهای آن، می‌توان این قابلیت را هم فعال کرد. اطلاعات بیشتر).
نکته‌ی مهم treeGrid، سه پارامتر دیگر هستند که از سمت کلاینت به سرور ارسال می‌شوند:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
using jqGrid13.Models;
using JqGridHelper.DynamicSearch; // for dynamic OrderBy
using JqGridHelper.Models;
using JqGridHelper.Utils;

namespace jqGrid13.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult GetComments(JqGridRequest request, int? nodeid, int? parentid, int? n_level)
        {
            var list = BlogCommentsDataSource.LatestBlogComments;

            // در این حالت خاص فعلا در نگارش جای جی‌کیو‌گرید صفحه بندی کار نمی‌کند و فعال نیست و محاسبات ذیل اهمیتی ندارند
            var pageIndex = request.page - 1;
            var pageSize = request.rows;
            var totalRecords = list.Count;
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var productsQuery = list.AsQueryable();
            
            if (nodeid == null)
            {
                productsQuery = productsQuery.Where(x => x.ParentId == null);
            }
            else
            {
                productsQuery = productsQuery.Where(x => x.ParentId == nodeid.Value);
            }

            var products = productsQuery.OrderBy(request.sidx + " " + request.sord)
                                        .Skip(pageIndex * pageSize)
                                        .Take(pageSize)
                                        .ToList();


            var newLevel = n_level == null ? 0 : n_level.Value + 1;
            var productsData = new JqGridData
            {
                Total = totalPages,
                Page = request.page,
                Records = totalRecords,
                Rows = (products.Select(comment => new JqGridRowData
                {
                    Id = comment.Id,
                    RowCells = new List<object> 
                               {
                                   comment.Id,
                                   comment.Body,
                                   comment.AddDateTime.ToPersianDate(),
                                   // اطلاعات خاص نمایش درختی به ترتیب
           newLevel,
           comment.ParentId == null ? "" : comment.ParentId.Value.ToString(CultureInfo.InvariantCulture),
           comment.IsNotExpandable,
           comment.IsExpanded
                               }
                })).ToList()
            };
            return Json(productsData, JsonRequestBehavior.AllowGet);
        }
    }
}
nodeid اگر نال بود، یعنی کل اطلاعات ریشه‌ها (مواردی که parentId مساوی نال دارند)، باید واکشی شوند. اگر nodeid مقدار داشت، یعنی فرزند نود جاری قرار است بازگشت داده شود.
n_level مقدار جلو رفتگی نمایش اطلاعات یک نود را مشخص می‌کند. در اینجا چون با کلیک بر روی هر نود، فرزند آن از سرور واکشی می‌شود و lazy loading برقرار است، بازگشت مقدار n_level دریافتی از کلاینت به علاوه یک، کافی است. اگر نیاز است تمام نودها باز شده نمایش داده شوند، این مورد را باید به صورت دستی محاسبه کرده و در مدل BlogComment پیش بینی کنید.
در نهایت آرایه‌ای از خواص مدنظر به همراه 4 خاصیت ساختار درختی باید به ترتیب بازگشت داده شوند.




فعال سازی سمت کاربر treeGrid

برای فعال سازی سمت کاربر نمایش درختی اطلاعات، باید سه خاصیت ذیل تنظیم شوند:
            $('#list').jqGrid({
                caption: "آزمایش سیزدهم",
                // .... مانند قبل
                treeGrid: true,
                treeGridModel: 'adjacency',
                ExpandColumn: '@(StronglyTyped.PropertyName<BlogComment>(x => x.Body))'
            }).jqGrid('gridResize', { minWidth: 400 });
        });
تنظیم treeGrid: true سبب فعال سازی treeGrid می‌شود. توسط treeGridModel حالات Nested Set model و Adjacency model قابل تنظیم هستند و ExpandColumn نام ستونی را مشخص می‌کند که قرار است فرزندان آن نمایش داده شوند.


یک نکته‌ی تکمیلی

اگر می‌خواهید دقیقا به شکل زیر برسید:

تنظیم rownumbers: true گرید را حذف کنید. همچنین ستون Id را نیز با تنظیم‌های hidden:true, key: true مخفی نمائید (در تعاریف colModel).


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


برای مطالعه بیشتر
Tree Grid
Nested Set Model
Adjacency Model
مطالب
OpenCVSharp #6
نمایش ویدیو و اعمال فیلتر بر روی آن

در قسمت قبل با نحوه‌ی نمایش تصاویر OpenCV در برنامه‌های دات نتی آشنا شدیم. در این قسمت قصد داریم همان نکات را جهت پخش یک ویدیو توسط OpenCVSharp بسط دهیم.


روش‌های متفاوت پخش ویدیو و یا کار با یک Capture Device

OpenCV امکان کار با یک WebCam، دوربین و یا فیلم‌های آماده را دارد. برای این منظور کلاس CvCapture در OpenCVSharp پیش بینی شده‌است. در اینجا قصد داریم جهت سهولت پیگیری بحث، یک فایل avi را به عنوان منبع CvCapture معرفی کنیم:
using (var capture = new CvCapture(@"..\..\Videos\drop.avi"))
{
     var image = capture.QueryFrame();
}
روش کلی کار با CvCapture را در اینجا ملاحظه می‌کنید. متد QueryFrame هربار یک frame از ویدیو را بازگشت می‌دهد و می‌توان آن‌را در یک حلقه، تا زمانیکه image نال بازگشت داده نشده، ادامه داد. همچنین برای نمایش آن نیز می‌توان از یکی از روش‌های مطرح شده، مانند picture box استاندارد یا PictureBoxIpl (روش توصیه شده) استفاده کرد. اگر از PictureBoxIpl استفاده می‌کنید، متد pictureBoxIpl1.RefreshIplImage آن دقیقا برای یک چنین مواردی طراحی شده‌است تا سربار نمایش تصاویر را به حداقل برساند.
در اینجا اولین روشی که جهت به روز رسانی UI به نظر می‌رسد، استفاده از متد Application.DoEvents است تا UI فرصت داشته باشد، تعداد فریم‌های بالا را نمایش دهد و خود را به روز کند:
IplImage image;
while ((image = Capture.QueryFrame()) != null)
{
    _pictureBoxIpl1.RefreshIplImage(image);
 
    Thread.Sleep(interval);
    Application.DoEvents();
}
این روش هرچند کار می‌کند اما همانند روش استفاده از متد رخدادگردان Application Do Idle که صرفا در زمان بیکاری برنامه فراخوانی می‌شود، سبب خواهد شد تا تعدادی فریم را از دست دهید، همچنین با CPU Usage بالایی نیز مواجه شوید.
روش بعدی، استفاده از یک تایمر است که Interval آن بر اساس نرخ فریم‌های ویدیو تنظیم شده‌است:
timer = new Timer();
timer.Interval = (int)(1000 / Capture.Fps);
timer.Tick += Timer_Tick;
این روش بهتر است از روش DoEvents و به خوبی کار می‌کند؛ اما باز هم کار دریافت و همچنین پخش فریم‌ها، در ترد اصلی برنامه انجام خواهد شد.
روش بهتر از این، انتقال دریافت فریم‌ها به تردی جداگانه و پخش آن‌ها در ترد اصلی برنامه است؛ زیرا نمی‌توان GUI را از طریق یک ترد دیگر به روز رسانی کرد. برای این منظور می‌توان از BackgroundWorker دات نت کمک گرفت. رخ‌داد DoWork آن در تردی جداگانه و مجزای از ترد اصلی برنامه اجرا می‌شود، اما رخ‌داد ProgressChanged آن در ترد اصلی برنامه اجرا شده و امکان به روز رسانی UI را فراهم می‌کند.


استفاده از BackgroundWorker جهت پخش ویدیو به کمک OpenCVSharp


ابتدا دو دکمه‌ی Start و Stop را به فرم اضافه خواهیم کرد (شکل فوق).
سپس در زمان آغاز برنامه، یک PictureBoxIpl را به فرم جاری اضافه می‌کنیم:
private void FrmMain_Load(object sender, System.EventArgs e)
{
    _pictureBoxIpl1 = new PictureBoxIpl
    {
        AutoSize = true
    };
    flowLayoutPanel1.Controls.Add(_pictureBoxIpl1);
}
و یا همانطور که در قسمت پیشین نیز عنوان شد، می‌توانید این کنترل را به نوار ابزار VS.NET اضافه کرده و سپس به سادگی آن‌را روی فرم قرار دهید.

در دکمه‌ی Start، کار آغاز BackgroundWorker انجام خواهد شد:
private void BtnStart_Click(object sender, System.EventArgs e)
{
    if (_worker != null && _worker.IsBusy)
    {
        return;
    }
 
    _worker = new BackgroundWorker
    {
        WorkerReportsProgress = true,
        WorkerSupportsCancellation = true
    };
    _worker.DoWork += workerDoWork;
    _worker.ProgressChanged += workerProgressChanged;
    _worker.RunWorkerCompleted += workerRunWorkerCompleted;
    _worker.RunWorkerAsync();
 
    BtnStart.Enabled = false;
}
در اینجا یک سری خاصیت را مانند امکان لغو عملیات، جهت استفاده‌ی در دکمه‌ی Stop، به همراه تنظیم رخ‌دادگردان‌هایی جهت دریافت و نمایش فریم‌ها تعریف کرده‌ایم. کدهای این روال‌های رخدادگردان را در ادامه ملاحظه می‌کنید:
private void workerDoWork(object sender, DoWorkEventArgs e)
{
    using (var capture = new CvCapture(@"..\..\Videos\drop.avi"))
    {
        var interval = (int)(1000 / capture.Fps);
 
        IplImage image;
        while ((image = capture.QueryFrame()) != null &&
                _worker != null && !_worker.CancellationPending)
        {
            _worker.ReportProgress(0, image);
            Thread.Sleep(interval);
        }
    }
}
 
private void workerProgressChanged(object sender, ProgressChangedEventArgs e)
{
    var image = e.UserState as IplImage;
    if (image == null) return;
 
    Cv.Not(image, image);
    _pictureBoxIpl1.RefreshIplImage(image);
}
 
private void workerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    _worker.Dispose();
    _worker = null;
    BtnStart.Enabled = true;
}
متد workerDoWork کار دریافت فریم‌ها را در یک ترد مجزای از ترد اصلی برنامه به عهده دارد. این فریم‌ها توسط متد ReportProgress به متد workerProgressChanged جهت نمایش نهایی ارسال خواهند شد. این متد در ترد اصلی برنامه اجرا می‌شود و در اینجا کار با UI، مشکلی را به همراه نخواهد داشت و برنامه کرش نمی‌کند. اگر در متد workerDoWork کار به روز رسانی UI را مستقیما انجام دهیم، چون ترد اجرایی آن، با ترد اصلی برنامه یکی نیست، برنامه بلافاصله کرش خواهد کرد.
متد workerRunWorkerCompleted در پایان کار نمایش ویدیو، به صورت خودکار فراخوانی شده و در اینجا می‌توانیم دکمه‌ی Start را مجددا فعال کنیم.
همچنین در حین نمایش ویدیو، با کلیک بر روی دکمه‌ی Stop، می‌توان درخواست لغو عملیات را صادر کرد:
private void BtnStop_Click(object sender, System.EventArgs e)
{
    if (_worker != null)
    {
        _worker.CancelAsync();
        _worker.Dispose();
    }
    BtnStart.Enabled = true;
}


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب دوره‌ها
دسترسی سریع به مقادیر خواص توسط Reflection.Emit
اگر پروژه‌های چندسال اخیر را مرور کرده باشید خصوصا در زمینه ORMها و یا Serializerها و کلا مواردی که با Reflection زیاد سروکار دارند، تعدادی از آن‌ها پیشوند fast را یدک می‌کشند و با ارائه نمودارهایی نشان می‌دهند که سرعت عملیات و کتابخانه‌های آن‌ها چندین برابر کتابخانه‌های معمولی است و ... سؤال مهم اینجا است که رمز و راز این‌ها چیست؟
فرض کنید تعاریف کلاس User به صورت زیر است:
public class User
{
     public int Id { set; get; }
}
همانطور که در قسمت‌های قبل نیز عنوان شد، خاصیت Id در کدهای IL نهایی به صورت متدهای get_Id و set_Id ظاهر می‌شوند.
حال اگر یک متد پویا ایجاد کنیم که بجای هر بار Reflection جهت دریافت مقدار Id، خود متد get_Id را مستقیما صدا بزند، چه خواهد شد؟
پیاده سازی این نکته را در ادامه ملاحظه می‌کنید:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;

namespace FastReflectionTests
{
    /// <summary>
    /// کلاسی برای اندازه گیری زمان اجرای عملیات
    /// </summary>
    public class Benchmark : IDisposable
    {
        Stopwatch _watch;
        string _name;

        public static Benchmark Start(string name)
        {
            return new Benchmark(name);
        }

        private Benchmark(string name)
        {
            _name = name;
            _watch = new Stopwatch();
            _watch.Start();
        }

        public void Dispose()
        {
            _watch.Stop();
            Console.WriteLine("{0} Total seconds: {1}"
                               , _name, _watch.Elapsed.TotalSeconds);
        }
    }

    public class User
    {
        public int Id { set; get; }
    }

    class Program
    {
        public static Func<object, object> GetFastGetterFunc(string propertyName, Type ownerType)
        {
            var propertyInfo = ownerType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);

            if (propertyInfo == null)
                return null;
            
            var getter = ownerType.GetMethod("get_" + propertyInfo.Name,
                                             BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
            if (getter == null)
                return null;

            var dynamicGetterMethod = new DynamicMethod(
                                                name: "_",
                                                returnType: typeof(object),
                                                parameterTypes: new[] { typeof(object) },
                                                owner: propertyInfo.DeclaringType,
                                                skipVisibility: true);
            var il = dynamicGetterMethod.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0); // Load input to stack
            il.Emit(OpCodes.Castclass, propertyInfo.DeclaringType); // Cast to source type
            // نکته مهم در اینجا فراخوانی نهایی متد گت بدون استفاده از ریفلکشن است
            il.Emit(OpCodes.Callvirt, getter); //calls its get method

            if (propertyInfo.PropertyType.IsValueType)
                il.Emit(OpCodes.Box, propertyInfo.PropertyType);//box

            il.Emit(OpCodes.Ret);

            return (Func<object, object>)dynamicGetterMethod.CreateDelegate(typeof(Func<object, object>));
        }


        static void Main(string[] args)
        {
            //تهیه لیستی از داده‌ها جهت آزمایش
            var list = new List<User>();
            for (int i = 0; i < 1000000; i++)
            {
                list.Add(new User { Id = i });
            }

            // دسترسی به اطلاعات لیست به صورت متداول از طریق ریفلکشن معمولی
            var idProperty = typeof(User).GetProperty("Id");
            using (Benchmark.Start("Normal reflection"))
            {
                foreach (var item in list)
                {
                    var id = idProperty.GetValue(item, null);
                }
            }

            // دسترسی از طریق روش سریع دستیابی به اطلاعات خواص
            var fastIdProperty = GetFastGetterFunc("Id", typeof(User));
            using (Benchmark.Start("Fast Property"))
            {
                foreach (var item in list)
                {
                    var id = fastIdProperty(item);
                }
            }
        }
    }
}
توضیحات:
از کلاس Benchmark برای نمایش زمان انجام عملیات دریافت مقادیر Id از یک لیست، به دو روش Reflection متداول و روش صدا زدن مستقیم متد get_Id استفاده شده است.
در متد GetFastGetterFunc، ابتدا به متد get_Id خاصیت Id دسترسی پیدا خواهیم کرد. سپس یک متد پویا ایجاد می‌کنیم تا این get_Id را مستقیما صدا بزند. حاصل کار را به صورت یک delegate بازگشت می‌دهیم. شاید عنوان کنید که در اینجا هم حداقل در ابتدای کار متد، یک Reflection اولیه وجود دارد. پاسخ این است که مهم نیست؛ چون در یک برنامه واقعی، تهیه delegates در زمان آغاز برنامه انجام شده و حاصل کش می‌شود. بنابراین در زمان استفاده نهایی، به هیچ عنوان با سربار Reflection مواجه نخواهیم بود.

خروجی آزمایش فوق بر روی سیستم معمولی من به صورت زیر است:
 Normal reflection Total seconds: 2.0054177
Fast Property Total seconds: 0.0552056
بله. نتیجه روش GetFastGetterFunc واقعا سریع و باور نکردنی است!


چند پروژه که از این روش استفاده می‌کنند
Dapper
AutoMapper
fastJson

در سورس این کتابخانه‌ها روش‌های فراخوانی مستقیم متدهای set نیز پیاده سازی شده‌اند که جهت تکمیل بحث می‌توان به آن‌ها مراجعه نمود.


ماخذ اصلی
این کشف و استفاده خاص، از اینجا شروع و عمومیت یافته است و پایه تمام کتابخانه‌هایی است که پیشوند fast را به خود داده‌اند:
2000% faster using dynamic method calls
بازخوردهای دوره
مدیریت تغییرات گریدی از اطلاعات به کمک استفاده از الگوی واحد کار مشترک بین ViewModel و لایه سرویس
- این پروژه بر اساس آخرین نگارش وابستگی‌های آن به روز شد و در این مخزن کد قرار گرفت.
- همچنین صفحه‌ی ویرایش کاربران آن نیز بررسی شد و با ویرایش یک سلول، دکمه‌ی ذخیره سازی فعال می‌شود و مشکلی نبود.
پاسخ به بازخورد‌های پروژه‌ها
نمایش چندی خطی یک فیلد
امکان تعریف سلول‌های سفارشی هم پیش بینی شده‌است. یک مثال
یک سلول سفارشی تعریف کنید که خودش یک PdfGrid است با 4 سطر یا بیشتر. بعد امکان تعریف padding و spacing برای سلول‌ها (ردیف‌های) این جدول تعریف شده، وجود دارد.