مطالب
جلوگیری از ورود نام Area های یکسان، در هنگام درج اطلاعات در برنامه‌های ASP.NET MVC 5x
در وب‌سایتی مثل آپارات، چنین آدرسی aparat.com/reporting به منزله‌ی آدرس دهی به کانال شخصیِ فردی است. حال اگر وب‌سایت ما نیز چنین سیستم آدرس دهی را داشته باشد و همچنین پیشتر یک Area با نام Reporting را نیز داشته باشیم، توسط چنین آدرسی (درحالت پیش فرض) به آن Area دسترسی خواهیم داشت:
mysite.com/reporting
حال اگر یکی از کاربران هنگام ساخت کانالی جدید (برای سناریوی بالا)، بخواهد آدرس کانالش Reporting باشد، با توجه به اینکه هم مسیر دسترسی به Area گزارشات (Reporting) و هم مسیر دسترسی به کانال این شخص از طریق Url بالا است، قطعا به مشکل خواهیم خورد.
برای رفع این مشکل میتوان یک فایل xml، txt و ... درست کرد و نام تمامی Area‌‌ها را در آن فایل ثبت کرد و بعد، هنگام ثبت کانال جدید (برای سناریوی بالا) توسط کاربر، فایل مذکور را خوانده و در صورتیکه نام آدرس وارد شده معادل یکی از Area‌‌های سایتمان بود و در لیست Area‌‌های از پیش ثبت شده در آن فایل قرار داشت، پیغام لازم را به کاربر نشان می‌دهیم و از ثبت و یا ویرایش اطلاعات، جلوگیری می‌کنیم.
روش فوق به درستی کار می‌کند و مشکلی ندارد، اما ضعف آن این است که به صورت دستی این عملیات باید انجام شود و در صورتیکه یک Area جدید اضافه شود، باید آن فایل ویرایش شود. اما می‌توان با استفاده از یک Attribute، این کار را انجام و تمامی عملیات را به صورت داینامیک انجام داد.
برای شروع، یک مدل برای کانال و یک منبع داده را برای آن در نظر می‌گیریم:
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        public string ChannelUrl { get; set; }
    }
}
using System.Collections.Generic;

namespace SampleProject.Models
{
    public static class ChannelDataSource
    {
        static ChannelDataSource() => Channels = new List<Channel>();
        public static List<Channel> Channels { get; private set; }
        public static void Add(Channel channel) => Channels.Add(channel);
    }
}
منبع داده، شامل یک خاصیت است که لیست تمامی کانال‌های از قبل اضافه شده را بر می‌گرداند و یک متد افزودن که به این لیست، یک کانال را اضافه می‌کند.
حال یک کنترلر به نام Channel را اضافه می‌کنیم:
using SampleProject.Models;
using System.Linq;
using System.Web.Mvc;

namespace SampleProject.Controllers
{
    public class ChannelController : Controller
    {
        // GET: Channel
        public ActionResult Index()
        {
            var channels = ChannelDataSource.Channels;
            return View(channels);
        }

        public ActionResult Channel(string channelUrl)
        {
            if (string.IsNullOrWhiteSpace(channelUrl))
            {
                return new HttpNotFoundResult("channel not found!");
            }
            var channel = ChannelDataSource.Channels.SingleOrDefault(ch => ch.ChannelUrl == channelUrl.ToLower());
            if (channel == null)
            {
                return new HttpNotFoundResult("channel not found!");
            }
            return View(channel);
        }

        public ActionResult Create() => View();

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Channel channel)
        {
            if (!ModelState.IsValid)
            {
                ModelState.AddModelError(string.Empty, "Please check your inputs!");
                return View(channel);
            }
            ChannelDataSource.Add(channel);
            TempData["Message"] = "Channel added successfully!";
            return RedirectToAction(nameof(Index));
        }
    }
}
در اکشن Index، لیستی از تمامی کانال‌های موجود را نمایش می‌دهیم. در اکشن Channel، آدرسی را که وارد شده است، در منبع داده به دنبال آن می‌گردیم و یک ویوو با Template جزئیات (Details)، از مدل کانال را به کاربر نمایش می‌دهیم؛ در غیر اینصورت صفحه 404 را نمایش می‌دهیم. در اکشن‌های Create، صفحه افزودن را به کاربر نمایش داده و در آن یکی اکشن، عمل افزودن را در صورتیکه اطلاعات وارد شده صحیح باشند، انجام می‌دهیم.
با توجه به اینکه میخواهیم سیستم مسیر دهی سایت برای کانال‌ها تغییر کند، فایل RouteConfig در پوشه‌ی App_Start را به شکل ذیل تغییر می‌دهیم:
using System.Web.Mvc;
using System.Web.Routing;

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

            routes.MapRoute(
                name: "ChannelUrls",
                url: "{channelurl}",
                defaults: new { controller = "Channel", action = "Channel", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Channel", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در مسیر دهی بالا اگر "نام سایت، اسلش، نام کانال" را وارد کند اولین سیستم مسیریابی فعال می‌شود و او را به اکشن Channel کنترلر Channel، راهنمایی می‌کند.
حال برای اینکه هنگام ساخت کانال جدید، نام تکراری یکی از Area‌ها را وارد نکند، به این ترتیب عمل می‌کنیم:
ابتدا یک متد کمکی را نوشته که لیست Area‌‌های پروژه‌مان را برگشت دهد ( + ):
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;

namespace SampleProject.Models
{
    public static class Utility
    {
        public static List<string> GetAllAreaNames()
        {
            var areaNames = RouteTable.Routes.OfType<Route>()
                            .Where(d => d.DataTokens != null)
                            .Where(d=> d.DataTokens.ContainsKey("area"))
                            .Select(r => r.DataTokens["area"].ToString().ToLower())
                            .ToList();
            return areaNames;
        }
    }
}
و بعد Attribute مورد نظر را ایجاد میکنیم:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using SampleProject.Models;

namespace SampleProject.CustomValidators
{
    public class CheckForAreaExisting : ValidationAttribute, IClientValidatable
    {
        public List<string> AreaNames
        {
            get
            {
                return Utility.GetAllAreaNames();
            }
        }
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ValidationType = "checkforareaexisting",
                ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
            };
            rule.ValidationParameters.Add("areanames", string.Join(",", AreaNames));
            yield return rule;
        }

        public override bool IsValid(object value)
        {
            if (value != null)
            {
                return Utility.GetAllAreaNames()
                   .SingleOrDefault(area => area == value.ToString().ToLower()) == null;
            }
            return true;
        }
    }
}
در کلاس بالا توسط متد IsValid بررسی میکنیم که آیا مقدار وارد شده ( Channel Url ) با یکی از نام‌های Area‌‌های پروژه‌مان تطابق دارد یا خیر، که اگر این چنین بود، مقدار false برگشت داده می‌شود.
توسط واسط IClientValidatable و متود GetClientValidationRules کارهای اعتبارسنجی سمت کلاینت را نیز انجام می‌دهیم ( + ). مقدار خاصیت ValidationType نام متدی است که در سمت کلاینت این کار را انجام می‌دهد. مقدار خاصیت ValidationParameters، مقداری است که به سمت کلاینت به عنوان param فرستاده می‌شود تا از آن جهت اینکه آیا مقدار وارد شده توسط کاربر، یکی از Area‌های سایت هست یا خیر، استفاده کرد. در اینجا نام Area‌‌‌ها را با یک رشته و با یک جداکننده، توسط این خاصیت به سمت کلاینت می‌فرستیم. 
حال در سمت کلاینت یک فایل Js را با نام CustomValidation و محتوای زیر ایجاد می‌کنیم:
jQuery.validator.addMethod("checkforareaexisting",
    function (value, element, param) {
        var isValueOneOfTheAreaNames = $.inArray(value.toLowerCase(), param.areaNames) === -1;
        return isValueOneOfTheAreaNames;
    });

$.validator.unobtrusive.adapters.add('checkforareaexisting', ['areanames'], function (options) {
    options.rules['checkforareaexisting'] = { areaNames: options.params.areanames.split(',') };
    options.messages['checkforareaexisting'] = options.message;
});
در بخش اول، نام متد که در بالا (Attribute) به آن اشاره شده است آمده است، و بعد بررسی می‌کنیم که آیا مقدار آمده توسط کاربر، یکی از نام‌های Area‌‌های موجود سایت است یا خیر که اگر این طور باشد، false برگشت داده می‌شود و پیغام خطا به کاربر نمایش داده می‌شود. در بخش Onubtrusive توسط پارامتری که در Attribute برای فرستادن نام Area‌ها نوشته بودیم (areanames)، نام‌های Area‌ها را می‌گیریم و بعد آن را Split و به Rule انتساب می‌دهیم و ErrorMessage ـی را که به خاصیت ChannelUrl مدلمان نسبت می‌دهیم، به عنوان پیغام خطا در نظر می‌گیریم.
فایل‌های Js در Layout باید به این صورت باشند:
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>_Layout</title>
    <style>

    </style>
</head>
<body>
    <div>
        @RenderBody()
    </div>
    <script src="~/Scripts/Jquery.js"></script>
    <script src="~/Scripts/jquery.validate.min.js"></script>
    <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
    <script src="~/Scripts/CustomValidation.js"></script>
</body>
</html>

حال کافی است به خاصیت ChannelUrl مدلمان این Attribute را نسبت دهیم:
using SampleProject.CustomValidators;
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [CheckForAreaExisting(ErrorMessage = "You can't use this url for your channel!")]
        public string ChannelUrl { get; set; }
    }
}
اکنون نوبت آزمایش برنامه است. کافی است که یک یا چند Area جدید را با نام‌های متفاوت، اضافه کنید و الان اگر به صفحه افزودن کانال مراجعه کنید و نام یکی از Area‌‌های سایت را در قسمت Channel Url وارد کنید، پیغام خطا نمایش داده می‌شود.
نکته: در این حالت اسامی تمامی Area‌‌های سایت به کلاینت ارسال می‌شود. اگر از این بابت احساس رضایت نمی‌کنید، میتوانید از خاصیت Remote توکار MVC بهره ببرید.
برای اینکار این اکشن را به کنترلر Channel اضافه می‌کنیم:
[HttpPost]
public ActionResult CheckForAreaExisting(string channelUrl)
{
    var isValueOneOfTheAreaNames = Utility.GetAllAreaNames()
                                   .SingleOrDefault(area => area == channelUrl.ToLower()) == null;
    return Json(isValueOneOfTheAreaNames);
}  
و بعد مدل نیز به این صورت تغییر می‌کند:
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [Remote("CheckForAreaExisting", "Channel",
            ErrorMessage = "You can't use this url for your channel!",
            HttpMethod = "Post")]
        public string ChannelUrl { get; set; }
    }
}
به این ترتیب هر بار درخواستی به سمت سرور ارسال و طی آن بررسی می‌شود که مقدار وارد شده یکی از Area‌‌‌‌های سایت هست یا خیر؟ بدیهی است که در این حالت، دیگر نیازی به واسط IClientValidatable در کلاس CheckForAreaExisting موجود در پوشه CustomValidators وجود ندارد.
نظرات مطالب
استفاده از افزونه‌ی jsTree در ASP.NET MVC
سلام من این تایپیک‌ها رو بررسی کردم ولی نتونستم این فرایندها رو ادغام کنم ببینید من داخل همین پروژه شما دو تا مدل با نام‌های Category.cs وDataBaseContext.cs تعریف کردم و کد هاشم بصورت زیر است حالا چه جوری با تابع بازگشتی داخل HomeController بجای populatetree اظلاعات رو دریافت کنم در ضمن فیلدها هم طبق گفته خودتون باید مطابق jstree باشه که در مل‌ها تعریف کردم؟
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;

namespace MvcJSTree.Models.Entities
{
    public class Category
    {
        public Category()
        {

        }
        public Category(string id,string parentid,string orginialid,string text,string position,string href)
        {
            this.Id = id;
            this.ParentId = parentid;
            this.OriginalId = orginialid;
            this.Text = text;
            this.Position = position;
            this.Href = href;
        }
               
        public string Id { set; get; }
        public string ParentId { set; get; }
        public string OriginalId { set; get; }
        public string Text { set; get; }
        public string Position { set; get; }
        public string Href { set; get; }

        public override string ToString()
        {
            return string.Format("{0},{1},{2},{3},{4},{5}",this.Id,this.ParentId,this.OriginalId,this.Text,this.Position,this.Href);
        }      
    }
    
}

کد مربوط به DataBaseContext
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
using System.Data.Entity;

namespace MvcJSTree.Models
{
    public class DataBaseContext:System.Data.Entity.DbContext
    {
        public DataBaseContext()
        {

        }
         static DataBaseContext ()
        {

            System.Data.Entity.Database.SetInitializer(
                new System.Data.Entity.DropCreateDatabaseIfModelChanges<DataBaseContext>());

        }
        
         public System.Data.Entity.DbSet<DataBaseContext> Category { get; set; }
        
    }
}
مطالب
CAPTCHAfa
حتماً با CAPTCHA آشنا هستید. فرایندی که در طی آن متنی نمایش داده می‌شود که عمدتاً فقط یک انسان قادر به درک و پاسخگویی به آن است. با این کار از ارسال داده‌های بیهوده توسط ربات‌ها جلوگیری می‌شود.
reCAPTCHA ایده‌ای است که با نمایش کلمات واقعی و اسکن شده از کتاب‌های قدیمی، بخشی از مشکلات را حل کرده و از کاربران اینترنت برای شناسایی کلماتی که رایانه توانایی خواندن آن‌ها را ندارد استفاده می‌کند. (شکل زیر)

با وارد کردن درست هر کلمه، بخشی از یک کتاب، روزنامه، و یا مجله‌ی قدیمی در رایانه شناسایی و به فرمت دیجیتال ذخیره می‌شود. به این شکل شما در دیجیتالی کردن متون کاغذی سهیم هستید.
پروژه‌ی reCAPTCHA توسط گوگل حمایت می‌شود و در این آدرس قرار دارد.
به تازگی تیمی از دانشکده فنی دانشگاه تهران به همراه انستیتو تکنولوژی ایلینویز شیکاگو، پروژه ای بر همین اساس اما برای متون فارسی با عنوان CAPTCHAfa تولید کرده اند (شکل زیر) که در این آدرس در دسترس است. امیدوارم این پروژه به گونه ای تغییر کنه که برای دیجیتالی کردن متن‌های پارسی استفاده بشه. در حال حاضر، این پروژه از کلماتی از پیش تعریف شده استفاده می‌کنه.


متاسفانه این پروژه در حال حاضر فقط توسط برنامه‌های PHP قابل استفاده است. از این رو بر آن شدم تا اون رو برای برنامه‌های ASP.NET (هم Web Forms و هم MVC) آماده کنم. برای استفاده از CAPTCHAfa نیاز به یک کلید خصوصی و یک کلید عمومی دارید که از این آدرس قابل دریافت است.
کدهای پروژه‌ی Class Library به شرح زیر است.
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//                    Captchafa demo for ASP.NET applications
//                                 by: Behrouz Rad
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
namespace Captcha
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Net;
    using System.Text;
    using System.Web;

    public class Captchafa
    {
        private static readonly string PRIVATE_KEY = "your private key";
        private static readonly string PUBLIC_KEY = "your public key";
        private static readonly string CAPTCHAFA_API_SERVER = "http://www.captchafa.com/api";
        private static readonly string CAPTCHAFA_VERIFY_SERVER = "http://www.captchafa.com/api/verify/";

        private IDictionary<string, string> CaptchafaData
        {
            get
            {
                HttpContext httpContext = HttpContext.Current;

                string remoteIp = httpContext.Request.ServerVariables["REMOTE_ADDR"];
                string challenge = httpContext.Request.Form["captchafa_challenge_field"];
                string response = httpContext.Request.Form["captchafa_response_field"];

                IDictionary<string, string> data = new Dictionary<string, string>() 
                { 
                    {"privatekey" , PRIVATE_KEY },
                    {"remoteip"   , remoteIp    },
                    {"challenge"  , challenge   },
                    {"response"   , response    }
                };

                return data;
            }
        }

        public static string CaptchafaGetHtml()
        {
            return string.Format("<script type=\"text/javascript\" src=\"{0}/?challenge&k={1}\"></script>", CAPTCHAFA_API_SERVER, PUBLIC_KEY);
        }

        public bool IsAnswerCorrect()
        {
            string dataToSend = this.CaptchafaPrepareDataToSend(this.CaptchafaData);

            string result = this.CaptchafaPostResponse(dataToSend);

            return result.StartsWith("true");
        }

        private string CaptchafaPrepareDataToSend(IDictionary<string, string> data)
        {
            string result = string.Empty;
            StringBuilder sb = new StringBuilder();

            foreach (var item in data)
            {
                sb.AppendFormat("{0}={1}&", item.Key, HttpUtility.UrlEncode(item.Value.Replace(@"\", string.Empty)));
            }

            result = sb.ToString();

            sb = null;

            result = result.Substring(0, result.LastIndexOf("&"));

            return result;
        }

        private string CaptchafaPostResponse(string data)
        {
            StreamReader reader = null;
            Stream dataStream = null;
            WebResponse response = null;
            string responseFromServer = string.Empty;

            try
            {
                WebRequest request = WebRequest.Create(CAPTCHAFA_VERIFY_SERVER);

                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";

                byte[] byteData = Encoding.UTF8.GetBytes(data);

                request.ContentLength = byteData.Length;

                dataStream = request.GetRequestStream();

                dataStream.Write(byteData, 0, byteData.Length);

                dataStream.Close();

                response = request.GetResponse();

                dataStream = response.GetResponseStream();

                reader = new StreamReader(dataStream);

                responseFromServer = reader.ReadToEnd();
            }
            finally
            {
                if (reader != null)
                {
                    reader.Close();
                }

                if (dataStream != null)
                {
                    dataStream.Close();
                }

                if (response != null)
                {
                    response.Close();
                }
            }

            return responseFromServer;
        }
    }
}
استفاده در پروژه‌های ASP.NET Web Forms
ابتدا ارجاعی به فایل Captchafa.dll ایجاد کنید و سپس در روال Page_Load، کد زیر را قرار دهید. این کار برای تزریق اسکریپ CAPTCHAfa به صفحه استفاده می‌شود. 
if (!IsPostBack)
{
    litCaptcha.Text = Captchafa.CaptchafaGetHtml();
}

litCaptcha، یک کنترل Literal است که اسکریپت تولید شده، به عنوان متن آن معرفی می‌شود.
بررسی صحت مقدار وارد شده توسط کاربر (مثلاً در روال Click یک دکمه) به صورت زیر است. 
Captchafa captchaFa = new Captchafa();

bool isAnswerCorrect = captchaFa.IsAnswerCorrect();

if (isAnswerCorrect)
{
    // پاسخ صحیح است
}
else
{
   // پاسخ صحیح نیست
}

استفاده در پروژه‌های ASP.NET MVC
ابتدا ارجاعی به فایل Captchafa.dll ایجاد کنید. در ASP.NET MVC بهتره تا فرایند کار رو در یک HTML helper کپسوله کنیم.
public static class CaptchaHelper
{
    public static MvcHtmlString Captchafa(this HtmlHelper htmlHelper)
    {
        return MvcHtmlString.Create(Captcha.Captchafa.CaptchafaGetHtml());
    }
}

بررسی صحت مقدار وارد شده توسط کاربر (پس از ارسال فرم به Server) به صورت زیر است.
[HttpPost]
[ActionName("Index")]
public ViewResult CaptchaCheck()
{
    Captchafa captchaFa = new Captchafa();

    bool isAnswerCorrect = captchaFa.IsAnswerCorrect();

    if (isAnswerCorrect)
    {
        ViewBag.IsAnswerCorrect = true;
    }
    else
    {
        ViewBag.IsAnswerCorrect = false;
    }

    return View();
}
و در نهایت، کدهای View (از سینتکس موتور Razor استفاده شده است).
@using CaptchafaDemoMvc.Helpers;

@{
    ViewBag.Title = "Index";
}

<form action="/" method="post">

@Html.Captchafa();

<input type="submit" id="btnCaptchafa" name="btnCaptchafa" value="آزمایش" />

@{
    bool isAnswerExists = ViewBag.IsAnswerCorrect != null;
}

@if (isAnswerExists)
{
    if ((bool)ViewBag.IsAnswerCorrect == true)
    {
       <span id="lblResult">پاسخ صحیح است</span>
    }
    else
    {
       <span id="lblResult">پاسخ صحیح نیست</span>
    }
}
</form>

دموی پروژه رو در این آدرس قرار دادم. پروژه‌ی نمونه نیز از این آدرس قابل دریافت است. 

پ.ن: به زودی برخی بهبودها رو بر روی این پروژه انجام میدم.
مطالب
آشنایی با Refactoring - قسمت 11

قسمت یازدهم آشنایی با Refactoring به توصیه‌هایی جهت بالا بردن خوانایی تعاریف مرتبط با اعمال شرطی می‌پردازد.

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

عموما حین تعریف شرط‌های ترکیبی، هدف اصلی از تعریف آن‌ها پشت انبوهی از && و || گم می‌شود و برای بیان مقصود، نیاز به نوشتن کامنت خواهند داشت. مانند:

using System;

namespace Refactoring.Day11.EncapsulateConditional.Before
{
public class Element
{
private string[] Data { get; set; }
private string Name { get; set; }
private int CreatedYear { get; set; }

public string FindElement()
{
if (Data.Length > 1 && Name == "E1" && CreatedYear > DateTime.Now.Year - 1)
return "Element1";

if (Data.Length > 2 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2)
return "Element2";

return string.Empty;
}
}
}

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

using System;

namespace Refactoring.Day11.EncapsulateConditional.After
{
public class Element
{
private string[] Data { get; set; }
private string Name { get; set; }
private int CreatedYear { get; set; }

public string FindElement()
{
if (hasOneYearOldElement)
return "Element1";

if (hasTwoYearsOldElement)
return "Element2";

return string.Empty;
}

private bool hasTwoYearsOldElement
{
get { return Data.Length > 2 && Name == "RCA" && CreatedYear > DateTime.Now.Year - 2; }
}

private bool hasOneYearOldElement
{
get { return Data.Length > 1 && Name == "E1" && CreatedYear > DateTime.Now.Year - 1; }
}
}
}


همانطور که ملاحظه می‌کنید پس از این جایگزینی، خوانایی متد FindElement بهبود یافته است و برنامه نویس اگر 6 ماه بعد به این کدها مراجعه کند نخواهد گفت: «من این کدها رو نوشتم؟!»؛ چه برسد به سایرینی که احتمالا قرار است با این کدها کار کرده و یا آن‌ها را نگهداری کنند.


ب) از تعریف خواص Boolean با نام‌های منفی پرهیز کنید

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

namespace Refactoring.Day11.RemoveDoubleNegative.Before
{
public class Customer
{
public decimal Balance { get; set; }

public bool IsNotFlagged
{
get { return Balance > 30m; }
}
}
}

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

namespace Refactoring.Day11.RemoveDoubleNegative.After
{
public class Customer
{
public decimal Balance { get; set; }

public bool IsFlagged
{
get { return Balance <= 30m; }
}
}
}

نظرات مطالب
استفاده ازExpressionها جهت ایجاد Strongly typed view در ASP.NET MVC
با تشکر از شما. حالت پیشرفته‌تر این مساله، کار با مدل‌های تو در تو هست. برای مثال:
    public class CompanyModel
    {
        public int Id { get; set; }
        public string CompanyName { get; set; }
        public string CompanyAbbr { get; set; }

        public Product Product { set; get; }
    }

    public class Product
    {
        public int Id { set; get; }
    }
در اینجا اگر بخواهیم Product.Id را بررسی کنیم:
var data = PropertyExtensions.PropertyName<CompanyModel>(x => x.Product.Id);
فقط Id آن دریافت می‌شود.
راه حلی که از کدهای EF برای این مساله استخراج شده به صورت زیر است (نمونه‌اش متد Include تو در تو بر روی چند خاصیت):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace PropertyExtensionsApp
{
    public class PropertyHelper : ExpressionVisitor
    {
        private Stack<string> _stack;
        public string GetNestedPropertyPath(Expression expression)
        {
            _stack = new Stack<string>();
            Visit(expression);
            return _stack.Aggregate((s1, s2) => s1 + "." + s2);
        }

        protected override Expression VisitMember(MemberExpression expression)
        {
            if (_stack != null)
                _stack.Push(expression.Member.Name);
            return base.VisitMember(expression);
        }

        public string GetNestedPropertyName<TEntity>(Expression<Func<TEntity, object>> expression)
        {
            return GetNestedPropertyPath(expression);
        }
    }
}
در این حالت خواهیم داشت:
var name = new PropertyHelper().GetNestedPropertyName<CompanyModel>(x => x.Product.Id);
که خروجی Product.Id را بر می‌گرداند.
بازخوردهای پروژه‌ها
عدم نمایش سطر آخر رکورد در pdf
سلام..مشکلی که دارم اینه که خروجی pdf من سطر آخررکودهای خروجی  رو توی فایل pdf بر نمیگردونه.چک کردم dt من مشکلی نداره..میخواستم راهنمایی کنید..

کد‌های من :
public IPdfReportData CreatePdfReport(DataTable dt,string OrganName,string DateFrom,string DateEnd,string FilePath)
        {
            FileStream fo = new FileStream(FilePath, FileMode.Create);
            fo.Close();
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "AjansDaneshjo", Application = "PdfRpt", Keywords = "Report", Subject = "Test Rpt", Title = "Report" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Application.StartupPath+"\\BNAZANIN.ttf", Application.StartupPath + "\\BNAZNNBD.ttf");
                fonts.Size(13);
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(PersianDate.ToPersianDateTime(DateTime.Now, "/", false, false));
            })
            .PagesHeader(header =>
            {
                header.CustomHeader(new MasterDetailsHeaders { PdfRptFont = header.PdfFont ,DateFrom=DateFrom,DateTo= DateEnd,ProjTitle=ProjTitle });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(50);
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = true,
                    SpacingBeforeAllGroupsSummary = 5f
                });
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.DataTable(dt);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings(" جمع کل ");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("Name_Family");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("در اختیار");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });

                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("DateAjans");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("تاریخ");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : PersianDate.ToPersianDateTime((DateTime)obj,"/",false,false) );
                    });
                });

                
                columns.AddColumn(column =>
                {
                    column.PropertyName("Receipt_number");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("شماره قبض");

                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("Price");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(5);
                    column.Width(2);
                    column.HeaderCell("مبلغ");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0} ریال", obj));
                    });
                });
                columns.AddColumn(column =>
                {
                    column.PropertyName("Organ_Name");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(5);
                    column.Width(2);
                    column.HeaderCell("نام سازمان");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                    
                });


                columns.AddColumn(column =>
                {
                    column.PropertyName("Destination");
                    column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("مقصد");

                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(fo.Name/*string.Format("{0}\\RptCalculatedFieldsSample-{1}.pdf", Application.StartupPath, Guid.NewGuid().ToString("N")))*/));
        }

کلاس MasterDetailsHeaders  :
public  class MasterDetailsHeaders : IPageHeader
    {
        public IPdfFont PdfRptFont { set; get; }
        public string DateFrom;
        public string DateTo;
        public string ProjTitle;
        public PdfGrid RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
           
            var table = new PdfGrid(numColumns: 1) { WidthPercentage = 100 };
            var organName = newGroupInfo.GetSafeStringValueOf("Organ_Name");
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = " گزارش " + organName + " از تاریخ " + DateFrom + " الی " + DateTo;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
                    cellProperties.RunDirection = PdfRunDirection.RightToLeft;
                });
            return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 5f);
        }
      public  PdfGrid RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            return null;
        }

مطالب
Soft Delete در Entity Framework 6
برای حذف نمودن یک رکورد از دیتابیس 2 راه وجود دارد : 1- حذف به صورت فیزیکی 2- حذف به صورت منطقی ( مورد بحث این مطلب )
در حذف رکورد به صورت منطقی، طراحان دیتابیس، فیلدی را با نام‌های متفاوتی همچون Flag , IsDeleted , IsActive , و غیره، در جداول ایجاد می‌نمایند. خوب، این روش مزایا و معایب خاص خودش را دارد. مثلا شما در هر پرس و جویی که ایجاد می‌نمایید، بایستی این مورد را چک نموده و رکوردهایی را فراخوانی نمایید که فیلد IsDeleted آن برابر با false باشد. و همچنین در زمان حذف رکورد، برنامه نویس بایستی از متد Update به جای حذف فیزیکی استفاده نماید که تمام این موارد حاکی از مشکلات خاص این روش است. 
در این مقاله سعی داریم که مشکلات ذکر شده در بالا را با ایجاد SoftDelete در EF 6 برطرف نماییم .*یکی از پیش نیاز‌های این پست مطالعه ( سری آموزشی EF CodeFirst ) در سایت جاری می‌باشد.
برای شروع، ما نیاز به داشتن یک Attribute برای مشخص ساختن موجودیت هایی داریم که بایستی بر روی آنها SoftDelete فعال گردد. پس برای اینکار کلاسی را به شکل زیر طراحی مینماییم:
using System.Data.Entity.Core.Metadata.Edm;
public class SoftDeleteAttribute : Attribute
    {
        public string ColumnName { get; set; }
        public SoftDeleteAttribute(string column)
        {
            ColumnName = column;
        }
        public static string GetSoftDeleteColumnName(EdmType type)
        {
            MetadataProperty column = type.MetadataProperties.Where(x => x.Name.EndsWith("customannotation:SoftDeleteColumnName")).SingleOrDefault();
            return column == null ? null : (string)column.Value;
        }
    }
توضیحات کد بالا: در متد سازنده، نام فیلدی را که قرار است بر روی آن SoftDelete به صورت اتوماتیک ایجاد شود، دریافت می‌نماییم و متد GetSoftDeleteColumnName در واقع با استفاده از متادیتاهایی که بر روی فیلد‌ها وجود دارد، فیلدی که انتهای نام آن متادیتای "customannotation:SoftDeleteColumnName" را دارد، انتخاب نموده و برگشت می‌دهد.
سؤال: متادیتای  "customannotation:SoftDeleteColumnName"  از کجا آمد؟ برای پاسخ به این سوال کافیست ادامه‌ی مطلب را کامل مطالعه نمایید.
حال این Attribute برای استفاده در موجودیت‌های ما آمده است. برای استفاده کافیست به روش زیر عمل نمایید .
    [SoftDelete("IsDeleted")]
    public class TblUser 
    {        
        [Key]
        public int TblUserID { get; set; }

        [MaxLength(30)]
        public string Name { get; set; }

        public bool IsDeleted { get; set; }
    }
برای معرفی این قابلیت جدید به EF 6 کافیست در DbContext برنامه در متد OnModelCreating به نحو زیر عمل نماییم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            var Conv = new AttributeToTableAnnotationConvention<SoftDeleteAttribute, string>(
                "SoftDeleteColumnName",
                (type, attribute) => attribute.Single().ColumnName);
            modelBuilder.Conventions.Add(Conv);

        }
در واقع ما در اینجا به Ef می‌گوییم که یک Annotation جدید، با نام SoftDeleteColumnName به Entity که توسط این Attribute مزین شده است، اضافه نماید و همچنین مقدار این Annotation را نام فیلدی که در متد سازنده SoftDeleteAttribute معرفی گردیده است قرار دهد.
برای اطمینان حاصل کردن از اینکه آیا Annotation جدید به مدل برنامه اضافه شده است یا نه کافیست بر روی فایل cs کانتکست DbContext، کلیک راست نموده و در منوی نمایش داده شده گزینه‌ی EntityFramework و سپس گزینه View Entity Data Model را انتخاب نمایید . مانند تصویر زیر:

در پنجره باز شده به قسمت سوم یعنی <StorageModels> مراجعه نمایید و بایستی گزینه زیر را مشاهده نمایید .

 <EntityType Name="TblUser" customannotation:SoftDeleteColumnName="IsDeleted">

تا اینجای کار ما توانستیم یک Annotation جدید را به Ef اضافه نماییم .

در مرحله بعد بایستی به Ef دستور دهیم که در تولید Query بر روی این Entity، این مورد را نیز لحاظ کند.

برای این کار کلاسی را ایجاد می‌نماییم که از اینترفیس IDbCommandTreeInterceptor ارث بری می‌نماید. مانند کد زیر :

public class SoftDeleteInterceptor : IDbCommandTreeInterceptor
    {
        public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext)
        {
            if (interceptionContext.OriginalResult.DataSpace == System.Data.Entity.Core.Metadata.Edm.DataSpace.SSpace)
            {
                var QueryCommand = interceptionContext.Result as DbQueryCommandTree;
                if (QueryCommand != null)
                {
                    var newQuery = QueryCommand.Query.Accept(new SoftDeleteQueryVisitor());
                    interceptionContext.Result = new DbQueryCommandTree(QueryCommand.MetadataWorkspace, QueryCommand.DataSpace, newQuery);
                }
            }
       }
}

در ابتدا تشخیص داده می‌شود که نوع خروجی Query آیا از نوع Storage Model است . ( برای توضیحات بیشتر ) سپس پرس و جوی تولید شده را با استفاده از الگوی visitor تغییر داده و Query جدید را تولید نموده و در انتها Query جدیدی را به جای Query قبلی جایگزین می‌نماییم.

در اینجا ما نیاز به داشتن کلاس  SoftDeleteQueryVisitor  برای تغییر دادن Query و اضافه نمودن IsDeleted <>1 به Query می‌باشیم.

یک کلاس دیگری با نام  SoftDeleteQueryVisitor  به شکل زیر  به برنامه اضافه می‌نماییم.

  public class SoftDeleteQueryVisitor : DefaultExpressionVisitor
    {
        public override DbExpression Visit(DbScanExpression expression)
        {
            var column = SoftDeleteAttribute.GetSoftDeleteColumnName(expression.Target.ElementType);
            if (column!=null)
            {
                var Binding = DbExpressionBuilder.Bind(expression);
                return DbExpressionBuilder.Filter(Binding, DbExpressionBuilder.NotEqual(DbExpressionBuilder.Property(DbExpressionBuilder.Variable(Binding.VariableType, Binding.VariableName), column), DbExpression.FromBoolean(true)));
            }
            else
            {
                return base.Visit(expression);
            }
        }
    }
در متد Visit تشخیص داده می‌شود که آیا Query ساخته شده دارای customannotation:SoftDeleteColumnName است؟ چنانچه این Annotation را دارا باشد، نام فیلدی را که بالای Entity ذکر شده است، بازگشت می‌دهد و در خط بعدی، نام این فیلد را با مقدار مخالف True به Query تولید شده اضافه می‌نماید.

در نهایت برای اینکه EF تشخیص دهد که یک‌چنین Interceptor ایی وجود دارد، بایستی در کلاس DbContextConfig، کلاس SoftDeleteInterceptor را اضافه نماییم؛ همانند کد زیر:

 public class DbContextConfig : DbConfiguration
    {
        public DbContextConfig()
        {
             AddInterceptor(new SoftDeleteInterceptor());
        }
    }

تا اینجا در تمام Query‌های تولید شده بر روی Entity که با خاصیت SoftDelete مزین شده است، مقدار IsDeleted <> 1 را به صورت اتوماتیک اعمال می‌نماید. حتی به صورت هوشمند چنانچه این موجودیت در یک Join استفاده شده باشد این شرط را قبل از Join به Query تولید شده اضافه می‌نماید.

در مقاله بعدی در مورد تغییر کد Remove به کد Update توضیح داده خواهد شد.


برای مطالعه بیشتر

Entity Framework: Building Applications with Entity Framework 6

نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
با سلام

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

public class Page
    {
        public virtual int Id { get; set; }
        public virtual string Title { get; set; }
        public virtual DateTime? CreatedDate { get; set; }
        public virtual DateTime? ModifiedDate { get; set; }
        public virtual string Body { get; set; }
        public virtual string Keyword { get; set; }
        public virtual string Description { get; set; }
        public virtual string Status { get; set; }
        public virtual bool? CommentStatus { get; set; }
        public virtual int? Order { get; set; }
        public virtual User User { get; set; }
        public virtual User EditedByUser { get; set; }
        public virtual ICollection<Comment> Comments { get; set; }
        public virtual int? ParentId { get; set; }
        public virtual Page Parent { get; set; }
        public virtual ICollection<Page> Children { get; set; }
    }

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

this._pages.ToList().Where(page => page.Parent == null).ToList();

دستور فوق به خوبی کار می‌کنه. ولی وقتی با که دستوراتی که توسط mini-profiler لاگ شده را می‌بینیم که اخطار duplicate reader را می‌دهد.
برای هر page موجود دستور زیر را صادر می‌کند
SELECT 
[Extent1].[Id] AS [Id], 
[Extent1].[Title] AS [Title], 
[Extent1].[CreatedDate] AS [CreatedDate], 
[Extent1].[ModifiedDate] AS [ModifiedDate], 
[Extent1].[Body] AS [Body], 
[Extent1].[Keyword] AS [Keyword], 
[Extent1].[Description] AS [Description], 
[Extent1].[Status] AS [Status], 
[Extent1].[CommentStatus] AS [CommentStatus], 
[Extent1].[Order] AS [Order], 
[Extent1].[ParentId] AS [ParentId], 
[Extent2].[Id] AS [Id1], 
[Extent2].[Title] AS [Title1], 
[Extent2].[CreatedDate] AS [CreatedDate1], 
[Extent2].[ModifiedDate] AS [ModifiedDate1], 
[Extent2].[Body] AS [Body1], 
[Extent2].[Keyword] AS [Keyword1], 
[Extent2].[Description] AS [Description1], 
[Extent2].[Status] AS [Status1], 
[Extent2].[CommentStatus] AS [CommentStatus1], 
[Extent2].[Order] AS [Order1], 
[Extent2].[ParentId] AS [ParentId1], 
[Extent2].[User_Id] AS [User_Id], 
[Extent2].[EditedByUser_Id] AS [EditedByUser_Id], 
[Extent1].[User_Id] AS [User_Id1], 
[Extent1].[EditedByUser_Id] AS [EditedByUser_Id1]
FROM  [dbo].[Pages] AS [Extent1]
LEFT OUTER JOIN [dbo].[Pages] AS [Extent2] ON [Extent1].[ParentId] = [Extent2].[Id

می‌خواستم ببینم کاری میشه کرد تا سربار این کوئری را کمتر کرد؟

در ضمن اگر بخواهم viewmodel را طوری تعریف کنم تا فیلدهای اضافی مانند createddate و user و... که در هنگام نمایش منوی آبشاری به آنها نیازی ندارم چه کار باید کرد؟ چون من هر کاری کردم نتونستم parent را برای viewmodel به خوبی تعریف کنم.
ممنون
مطالب
استخراج متن از فایل‌های PDF توسط iTextSharp
پیشنیاز
نحوه ذخیره شدن متن در فایل‌های PDF

حتما نیاز است پیشنیاز فوق را یکبار مطالعه کنید تا علت خروجی‌های متفاوتی را که در ادامه ملاحظه خواهید نمود، بهتر مشخص شوند. همچنین فایل PDF ایی که مورد بررسی قرار خواهد گرفت، همان فایلی است که توسط متد writePdf ذکر شده در پیشنیاز تهیه شده است.

دو کلاس متفاوت برای استخراج متن از فایل‌های PDF در iTextSharp وجود دارند:
الف) SimpleTextExtractionStrategy

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

namespace TestReaders
{
    class Program
    {
        private static void readPdf1()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
               var text = PdfTextExtractor.GetTextFromPage(reader, i, new SimpleTextExtractionStrategy());
                File.WriteAllText("page-" + i + "-text.txt", text);
            }
            reader.Close();
        }

        static void Main(string[] args)
        {
            readPdf1();
        }
    }
}
مثال فوق، متن موجود در تمام صفحات یک فایل PDF را در فایل‌های txt جداگانه‌ای ثبت می‌کند. برای نمونه اگر از PDF پیشنیاز یاد شده استفاده کنیم، خروجی آن به نحو زیر خواهد بود:
 Test
ld Wor llo He
Hello People
علت آن نیز پیشتر بررسی گردید. متن، در این فایل ویژه در مختصات خاصی ترسیم شده است. حاصل از دیدگاه خواننده نهایی بسیار خوانا است؛ اما خروجی hello world متنی جالبی از آن استخراج نمی‌شود. SimpleTextExtractionStrategy دقیقا بر اساس همان عملگر‌های Tj و همچنین منابع صفحه، عبارات را یافته و سر هم می‌کند.


ب) LocationTextExtractionStrategy

همان مثال قبل را درنظر بگیرید، اینبار به شکل زیر:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());
                File.WriteAllText("page-" + i + "-text.txt", text);
            }
            reader.Close();
        }
کلاس LocationTextExtractionStrategy هوشمند‌تر عمل کرده و بر اساس عملگرهای هندسی یک فایل PDF، سعی می‌کند جملات و حروف را کنار هم قرار دهد و در نهایت خروجی متنی بهتری را تولید کند. برای نمونه اینبار خروجی متنی حاصل به صورت زیر خواهد بود:
 Test
Hello World
Hello People
این خروجی با آنچه که در صفحه نمایش داده می‌شود تطابق دارد.


استخراج متون فارسی از فایل‌های PDF توسط iTextSharp

روش‌های فوق با PDFهای فارسی هم کار می‌کنند اما خروجی حاصل آن مفهوم نیست و نیاز به پردازش ثانوی دارد. ابتدا مثال زیر را درنظر بگیرید:
        static void writePdf2()
        {
            using (var document = new Document(PageSize.A4))
            {
                var writer = PdfWriter.GetInstance(document, new FileStream("test.pdf", FileMode.Create));
                document.Open();

                FontFactory.Register("c:\\windows\\fonts\\tahoma.ttf");
                var tahoma = FontFactory.GetFont("tahoma", BaseFont.IDENTITY_H);

                ColumnText.ShowTextAligned(
                            canvas: writer.DirectContent,
                            alignment: Element.ALIGN_CENTER,
                            phrase: new Phrase("تست می‌شود", tahoma),
                            x: 100,
                            y: 100,
                            rotation: 0,
                            runDirection: PdfWriter.RUN_DIRECTION_RTL,
                            arabicOptions: 0);                
            }

            Process.Start("test.pdf");
        }
از متد فوق، برای تولید یک فایل PDF که متنی فارسی را نمایش می‌دهد استفاده خواهیم کرد. اگر متد readPdf2 را که به همراه LocationTextExtractionStrategy تعریف شده است، بر روی فایل حاصل فراخوانی کنیم، خروجی آن به صورت زیر خواهد بود:
ﺩﻮﺷﻲﻣ ﺖﺴﺗ
برای تبدیل آن به یونیکد خواهیم داشت:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());                
                text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text));
                File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8);
            }
            reader.Close();
        }
اکنون خروجی ثبت شده در فایل متنی حاصل به صورت زیر است:
 ﺩﻮﺷﻲﻣ ﺖﺴﺗ
دقیقا به همان نحوی است که iTextSharp و اکثر تولید کننده‌های PDF فارسی از آن استفاده می‌کنند و اصطلاحا چرخاندن حروف یا تولید Glyph mirrors صورت می‌گیرد. روش‌های زیادی برای چرخاندن حروف وجود دارند. در ادامه از روشی استفاده خواهیم کرد که خود ویندوز در کارهای داخلی‌اش از آن استفاده می‌کند:
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;

namespace TestReaders
{
    [SuppressUnmanagedCodeSecurity]
    class GdiMethods
    {
        [DllImport("GDI32.dll")]
        public static extern bool DeleteObject(IntPtr hgdiobj);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern uint GetCharacterPlacement(IntPtr hdc, string lpString, int nCount, int nMaxExtent, [In, Out] ref GcpResults lpResults, uint dwFlags);

        [DllImport("GDI32.dll")]
        public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
    }

    [StructLayout(LayoutKind.Sequential)]
    struct GcpResults
    {
        public uint lStructSize;
        [MarshalAs(UnmanagedType.LPTStr)]
        public string lpOutString;
        public IntPtr lpOrder;
        public IntPtr lpDx;
        public IntPtr lpCaretPos;
        public IntPtr lpClass;
        public IntPtr lpGlyphs;
        public uint nGlyphs;
        public int nMaxFit;
    }

    public class UnicodeCharacterPlacement
    {
        const int GcpReorder = 0x0002;
        GCHandle _caretPosHandle;
        GCHandle _classHandle;
        GCHandle _dxHandle;
        GCHandle _glyphsHandle;
        GCHandle _orderHandle;

        public Font Font { set; get; }

        public string Apply(string lines)
        {
            if (string.IsNullOrWhiteSpace(lines))
                return string.Empty;

            return Apply(lines.Split('\n')).Aggregate((s1, s2) => s1 + s2);
        }

        public IEnumerable<string> Apply(IEnumerable<string> lines)
        {
            if (Font == null)
                throw new ArgumentNullException("Font is null.");

            if (!hasUnicodeText(lines))
                return lines;

            var graphics = Graphics.FromHwnd(IntPtr.Zero);
            var hdc = graphics.GetHdc();
            try
            {
                var font = (Font)Font.Clone();
                var hFont = font.ToHfont();
                var fontObject = GdiMethods.SelectObject(hdc, hFont);
                try
                {
                    var results = new List<string>();
                    foreach (var line in lines)
                        results.Add(modifyCharactersPlacement(line, hdc));
                    return results;
                }
                finally
                {
                    GdiMethods.DeleteObject(fontObject);
                    GdiMethods.DeleteObject(hFont);
                    font.Dispose();
                }
            }
            finally
            {
                graphics.ReleaseHdc(hdc);
                graphics.Dispose();
            }
        }

        void freeResources()
        {
            _orderHandle.Free();
            _dxHandle.Free();
            _caretPosHandle.Free();
            _classHandle.Free();
            _glyphsHandle.Free();
        }

        static bool hasUnicodeText(IEnumerable<string> lines)
        {
            return lines.Any(line => line.Any(chr => chr >= '\u00FF'));
        }

        void initializeResources(int textLength)
        {
            _orderHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _dxHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _caretPosHandle = GCHandle.Alloc(new int[textLength], GCHandleType.Pinned);
            _classHandle = GCHandle.Alloc(new byte[textLength], GCHandleType.Pinned);
            _glyphsHandle = GCHandle.Alloc(new short[textLength], GCHandleType.Pinned);
        }

        string modifyCharactersPlacement(string text, IntPtr hdc)
        {
            var textLength = text.Length;
            initializeResources(textLength);
            try
            {
                var gcpResult = new GcpResults
                {
                    lStructSize = (uint)Marshal.SizeOf(typeof(GcpResults)),
                    lpOutString = new String('\0', textLength),
                    lpOrder = _orderHandle.AddrOfPinnedObject(),
                    lpDx = _dxHandle.AddrOfPinnedObject(),
                    lpCaretPos = _caretPosHandle.AddrOfPinnedObject(),
                    lpClass = _classHandle.AddrOfPinnedObject(),
                    lpGlyphs = _glyphsHandle.AddrOfPinnedObject(),
                    nGlyphs = (uint)textLength,
                    nMaxFit = 0
                };
                var result = GdiMethods.GetCharacterPlacement(hdc, text, textLength, 0, ref gcpResult, GcpReorder);
                return result != 0 ? gcpResult.lpOutString : text;
            }
            finally
            {
                freeResources();
            }
        }
    }
}
از کلاس فوق در هر برنامه‌ای که راست به چپ را به نحو صحیحی پشتیبانی نمی‌کند، می‌توان استفاده کرد؛ خصوصا برنامه‌های گرافیکی.
در اینجا برای اصلاح متد readPdf2 خواهیم داشت:
        private static void readPdf2()
        {
            var reader = new PdfReader("test.pdf");
            int intPageNum = reader.NumberOfPages;
            for (int i = 1; i <= intPageNum; i++)
            {
                var text = PdfTextExtractor.GetTextFromPage(reader, i, new LocationTextExtractionStrategy());
                text = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(text));
                text = new UnicodeCharacterPlacement
                {
                    Font = new System.Drawing.Font("Tahoma", 12)
                }.Apply(text);
                File.WriteAllText("page-" + i + "-text.txt", text, Encoding.UTF8);
            }
            reader.Close();
        }
اگر خروجی متد اصلاح شده فوق را بررسی کنیم، دقیقا به «تست می‌شود» خواهیم رسید.

سؤال: آیا این روش با تمام PDFهای فارسی کار می‌کند؟
پاسخ: خیر! همانطور که در پیشنیاز مطلب جاری عنوان شد، در یک حالت خاص، PDF writer می‌تواند شماره Glyphها را کاملا عوض کرده و در فایل PDF نهایی ثبت کند. خروجی حاصل در برنامه Adobe reader خوانا است، چون نمایش را بر اساس اطلاعات هندسی Glyphها انجام می‌دهد؛ اما خروجی متنی آن به نوعی obfuscated است چون مثلا حرف A آن به کاراکتر مرسوم دیگری نگاشت شده است.
مطالب
قرار دادن نمودارهای MS Chart در گزارشات PdfReport
در حالت کلی، هر شیءایی را که بتوان تبدیل به تصویر کرد، قابلیت قرارگیری در یک فایل PDF را هم خواهد داشت. از این نمونه می‌توان به اشیاء MSChart اشاره کرد که از دات نت 4 جزئی از کتابخانه‌های اصلی دات نت شده‌اند و البته برای دات نت سه و نیم نیز به صورت جداگانه قابل دریافت است.
در ادامه مثالی را بررسی خواهیم کرد که بر اساس ردیف‌های گزارش آن، یک نمودار، به انتهای گزارش اضافه خواهد شد.
کدهای کامل این مثال را در ذیل مشاهده می‌کنید:
using System;
using System.Collections.Generic;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.ChartImage
{
    public class ChartImagePdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            var chart = new MSChartHelper
                {
                    AxisXTitle = "User",
                    AxisYTitle = "Balance",
                    ChartTitle = "Users Balance",
                    AxisTitleFont = new System.Drawing.Font("Tahoma", 12f),
                    LabelStyleFont = new System.Drawing.Font("Tahoma", 10f),
                    ChartTitleFont = new System.Drawing.Font("Arial", 16f, System.Drawing.FontStyle.Bold),
                    LegendsFont = new System.Drawing.Font("Tahoma", 12f)
                };

            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(string.Format("{0}\\fonts\\irsans.ttf", AppPath.ApplicationPath),
                            string.Format("{0}\\fonts\\verdana.ttf", Environment.GetEnvironmentVariable("SystemRoot")));
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.ClassicTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.Relative);
             })
             .MainTableDataSource(dataSource =>
             {
                 var listOfRows = new List<User>();
                 for (var i = 0; i < 7; i++)
                 {
                     listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = (i * 50) + 1000 });
                 }
                 dataSource.StronglyTypedList(listOfRows);
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
                 events.DocumentOpened(args =>
                 {
                     chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250);
                 });
                 events.RowAdded(args =>
                 {
                     if (args.RowType == RowType.DataTableRow)
                     {
                         var name = args.TableRowData.GetValueOf<User>(x => x.Name);
                         if (name == null) return;

                         var balance = args.TableRowData.GetValueOf<User>(x => x.Balance);
                         if (balance == null) return;

                         chart.AddXY(name, balance);
                     }
                 });
                 events.DocumentClosing(args =>
                 {
                     chart.AddChartToPage(args.PdfDoc);
                     chart.FreeResources();
                 });
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.Width(1);
                     column.HeaderCell("ردیف", captionRotation: 90);
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Id);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.Width(2);
                     column.HeaderCell("شماره");
                 });

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

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.LastName);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     column.Width(3);
                     column.HeaderCell("نام خانوادگی");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Balance);
                     column.HeaderCell("موجودی");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.Width(2);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                 });
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptChartSample.pdf"));
        }
    }
}
برای تهیه این گزارش و افزودن نمودار به آن، از کلاس کمکی ذیل استفاده شده است:
using System.Drawing;
using System.IO;
//It's part of the .NET 4.0+ now.
using System.Windows.Forms.DataVisualization.Charting;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.ChartImage
{
    public class MSChartHelper
    {
        // MS Chart learning tutorials:
        // http://weblogs.asp.net/scottgu/archive/2010/02/07/built-in-charting-controls-vs-2010-and-net-4-series.aspx
        Chart _chart;

        public System.Drawing.Font AxisTitleFont { set; get; }

        public string AxisXTitle { set; get; }

        public string AxisYTitle { set; get; }

        public string ChartTitle { set; get; }

        public System.Drawing.Font ChartTitleFont { set; get; }

        public System.Drawing.Font LabelStyleFont { set; get; }

        public System.Drawing.Font LegendsFont { set; get; }

        public void AddChartToPage(Document pdfDoc,
                                   int scalePercent = 100,
                                   float spacingBefore = 20,
                                   float spacingAfter = 10,
                                   float widthPercentage = 80)
        {
            using (var chartimage = new MemoryStream())
            {
                _chart.SaveImage(chartimage, ChartImageFormat.Bmp); //BMP gives the best compression result

                var iTextSharpImage = PdfImageHelper.GetITextSharpImageFromByteArray(chartimage.GetBuffer());
                iTextSharpImage.ScalePercent(scalePercent);
                iTextSharpImage.Alignment = Element.ALIGN_CENTER;

                var table = new PdfPTable(1)
                {
                    WidthPercentage = widthPercentage,
                    SpacingBefore = spacingBefore,
                    SpacingAfter = spacingAfter
                };
                table.AddCell(iTextSharpImage);

                pdfDoc.Add(table);
            }
        }

        public void AddXY(object xValue, params object[] yValue)
        {
            _chart.Series[0].Points.AddXY(xValue, yValue);
        }

        public void ChartInit(int width, int height)
        {
            _chart = new Chart
            {
                Width = width,
                Height = height,
                AntiAliasing = AntiAliasingStyles.All,
                TextAntiAliasingQuality = TextAntiAliasingQuality.High,
                Palette = ChartColorPalette.BrightPastel,
                BackColor = ColorTranslator.FromHtml("#F3DFC1"),
                BackGradientStyle = GradientStyle.TopBottom
            };

            setBorder();
            setTitles();
            setChartAreas();
            setLegends();
            setSeries();
        }

        public void FreeResources()
        {
            if (_chart != null && !_chart.IsDisposed)
                _chart.Dispose();
        }        

        private void setBorder()
        {
            _chart.BorderSkin.SkinStyle = BorderSkinStyle.Emboss;
            _chart.BorderlineWidth = 2;
            _chart.BorderlineColor = Color.FromArgb(181, 64, 1);
            _chart.BorderlineDashStyle = ChartDashStyle.Solid;
        }

        private void setChartAreas()
        {
            _chart.ChartAreas.Add("ChartArea1");
            _chart.ChartAreas[0].AxisX.Title = AxisXTitle;
            _chart.ChartAreas[0].AxisY.Title = AxisYTitle;
            _chart.ChartAreas[0].AxisX.TitleFont = AxisTitleFont;
            _chart.ChartAreas[0].AxisY.TitleFont = AxisTitleFont;
            _chart.ChartAreas[0].AxisX.LabelStyle.Font = LabelStyleFont;
            _chart.ChartAreas[0].AxisX.LabelStyle.Angle = -90;
            _chart.ChartAreas[0].BackColor = Color.White;
            _chart.ChartAreas[0].AxisX.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisY.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.FromArgb(64, 64, 64);
        }

        private void setLegends()
        {
            _chart.Legends.Add("Default");
            _chart.Legends[0].LegendStyle = LegendStyle.Row;
            _chart.Legends[0].IsTextAutoFit = false;
            _chart.Legends[0].DockedToChartArea = "ChartArea1";
            _chart.Legends[0].Docking = Docking.Bottom;
            _chart.Legends[0].IsDockedInsideChartArea = false;
            _chart.Legends[0].BackColor = Color.Transparent;
            _chart.Legends[0].Font = LegendsFont;
        }

        private void setSeries()
        {
            _chart.Series.Add("");
            _chart.Series[0].ChartType = SeriesChartType.Column;
            _chart.Series[0].Palette = ChartColorPalette.EarthTones;
            _chart.Series[0].IsValueShownAsLabel = true;
            _chart.Series[0].IsVisibleInLegend = false;
        }

        private void setTitles()
        {
            _chart.Titles.Add(ChartTitle);
            _chart.Titles[0].Font = ChartTitleFont;
            _chart.Titles[0].TextStyle = TextStyle.Shadow;
            _chart.Titles[0].ShadowOffset = 3;
            _chart.Titles[0].ShadowColor = Color.FromArgb(32, 0, 0);
            _chart.Titles[0].Alignment = ContentAlignment.TopCenter;
            _chart.Titles[0].ForeColor = Color.FromArgb(26, 59, 105);
        }
    }
}

توضیحات:
- استفاده از MSChart در اینجا از این جهت مناسب است که فراگیری کار کردن با آن عمومی بوده و در پروژه‌های وب و ویندوز کاربرد دارد و احتمالا هم اکنون با نحوه کارکردن با آن آشنا هستید، زیرا از سال 2010 به دات نت اضافه شده است.
- در این بین تنها متد جدید و مهم کلاس MSChartHelper، متد AddChartToPage آن است. به کمک متد chart.SaveImage می‌توان تصویر نهایی معادل یک نمودار را در حافظه ذخیره کرد. سپس با استفاده از متد PdfImageHelper.GetITextSharpImageFromByteArray، این تصویر موجود در حافظه را به معادل قابل استفاده آن در iTextSharp تبدیل کرده و به صفحه اضافه خواهیم کرد.
- در کدهای اصلی تولید گزارش، مقدار دهی کلاس کمکی MSChartHelper در قسمت رخدادهای قابل استفاده PdfReport در متد MainTableEvents آن انجام شده است:
در روال رویدادگردان DocumentOpened، بر اساس عرض واقعی صفحه، عرض نمودار را مشخص می‌کنیم:
                 events.DocumentOpened(args =>
                 {
                     chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250);

                 });
سپس در روال رویدادگردان RowAdded، فرصت خواهیم داشت به اطلاعات در حال افزوده شدن به گزارش دسترسی داشته باشیم. این اطلاعات را به متد افزودن XY نمودار ارسال خواهیم کرد:
                 events.RowAdded(args =>
                 {
                     if (args.RowType == RowType.DataTableRow)
                     {
                         var name = args.TableRowData.GetValueOf<User>(x => x.Name);
                         if (name == null) return;

                         var balance = args.TableRowData.GetValueOf<User>(x => x.Balance);
                         if (balance == null) return;

                         chart.AddXY(name, balance);
                     }
                 });
و در آخر، پیش از بسته شدن فایل PDF تولیدی (DocumentClosing)، نمودار نهایی را به صفحه اضافه کرده و منابع مرتبط با آن‌را آزاد خواهیم کرد:
                 events.DocumentClosing(args =>
                 {
                     chart.AddChartToPage(args.PdfDoc);
                     chart.FreeResources();
                 });
بنابراین اگر این سؤال عمومی وجود دارد که آیا می‌توان در این بین، به ابتدا و انتهای گزارش اشیایی را افزود، روش کلی آن‌را در روال‌های فوق ملاحظه می‌کنید. توسط args.PdfDoc و args.PdfWriter می‌توان به زیرساخت iTextSharp دسترسی یافت.