اشتراک‌ها
ایجاد ViewModel و View برای Caliburn.Micro به روش Scaffolding

بعضی وقت‌ها نوشتن ViewModel و یا طراحی View برای کلاس‌های Model تکراری و خسته کننده س. این پکیج به شما کمک می‌کنه بعضی کارهای تکراری مثل نوشتن Property‌های ViewModel و یا چیدن TextBox‌ها و وصل کردنشون به ViewModel رو انجام ندید.

البته باید اضافه کنم این پکیج فقط برای پروژه‌های WPF ای که Caliburn.Micro رو انتخاب کردند مناسبه چون توی ViewModel از کلاس PropertyChangedBase ارث بری می‌کنه و در View ها، با قاعده x:Name عمل Binding رو انجام میده.

شنونده پیشنهادات، انتقادات و گزارش باگ‌های شما برای این package  هستم :)

ایجاد ViewModel و View برای Caliburn.Micro به روش Scaffolding
مطالب
استفاده از پلاگین DataTables کتابخانه jQuery در برنامه‌های ASP.NET Core
datatable js، کتابخانه‌ای جهت ساخت جداول است و نسبت به رقیب اصلی خودش یعنی kendo telerik، از سادگی بیشتری برخوردار هست و امکانات خوبی هم دارد.

اگر برای جداول صفحات خود، از کتابخانه‌ی جی‌کوئری datatable استفاده میکنید، بعد از مدتی که تعداد رکورد‌ها زیاد میشوند، شاهد کند شدن صفحه خود خواهید شد. برای رفع این مشکل نیاز به پیاده سازی pagination دارید که به صورت خیلی ساده‌ای قابل پیاده سازی هست و شما تغییر کمی را در سمت سرور اعمال میکنید و سایر موارد توسط خود کتابخانه انجام میشود.

در ابتدا به بررسی کد‌ها و تغییرات سمت فرانت‌اند و صفحه‌ی cshtml می‌پردازیم:
1- تابع Ajax ای که وظیفه‌ی دریافت اطلاعات را دارد، به کل پاک کنید. چون Ajax به صورت یک آبجکت، به درون خود دیتاتیبل منتقل خواهد شد.
2- در صفحه خود، کد زیر را قرار دهید (جهت جلوگیری از 400 bad request) که این کار فقط برای هندلر‌های razor page و یا controller نیاز است و اگر از API استفاده میکنید، مسلما نیازی به این مدل تنظیمات نیست.
@Html.AntiForgeryToken()
3- سپس کد زیر را به startup خود اضافه کنید (در قسمتی که دارید اینترفیس‌ها را ثبت میکنید):
//Post in Ajax
services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
4- حالا نوبت کانفیگ‌های دیتاتیبل هست:
function initDataTables() {
        table.destroy();
        table = $("#tblJs").DataTable({
          processing: true,
          serverSide: true,
          filter: true,
          ajax: {
            url: '@Url.Page("yourPage","yourHandler")',
            beforeSend: function (xhr) {
              xhr.setRequestHeader("XSRF-TOKEN",
                $('input:hidden[name="__RequestVerificationToken"]').val());
            },
            type: "POST",
            datatype: "json"
          },
          language: {
            url: "/Persian.json"
          },
          responsive: true,
          select: true,

          columns: scheme,
          select: true,
        });
      }
5-کد بالا، کل تابعی را نشان میدهد که وظیفه‌ی ساخت دیتاتیبل را دارد؛ ولی شما تنها نیاز دارید قسمت زیر را اضافه کنید:
processing: true,
          serverSide: true,
          filter: true,
          ajax: {
            url: '@Url.Page("yourPage","yourHandler  ")',
            beforeSend: function (xhr) {
              xhr.setRequestHeader("XSRF-TOKEN",
                $('input:hidden[name="__RequestVerificationToken"]').val());
            },
            type: "POST",
            datatype: "json"
          },

حالا باید کد‌های سمت سرور را بنویسیم. برای این منظور باید ابتدا مقادیری را که دیتاتیبل برای ما ارسال میکند، از ریکوئست دریافت کنیم.
6- کل دیتایی که دیتا تیبل برای ما میفرستد، به مدل زیر خلاصه میشود:
public class FiltersFromRequestDataTable
    {
        public string length { get; set; }
        public string start { get; set; }
        public string sortColumn { get; set; }
        public string sortColumnDirection { get; set; }
        public string sortColumnIndex { get; set; }
        public string draw { get; set; }
        public string searchValue { get; set; }
        public int pageSize { get; set; }
        public int skip { get; set; }
    }
نکته‌ی مهم این است که پراپرتی‌ها باید با اسم کوچک به سمت فرانت‌اند ارسال شوند.
* (من از razor page  استفاده میکنم؛ ولی مسلما در controller هم به همین شکل و راحت‌تر خواهد بود) 
7- سپس داده‌های ارسال شده‌ی توسط دیتاتیبل، به سمت سرور را با استفاده از متد زیر دریافت میکنیم:
 public static void GetDataFromRequest(this HttpRequest Request, out FiltersFromRequestDataTable filtersFromRequest)
        {
            //TODO: Make Strings Safe String
            filtersFromRequest = new();

            filtersFromRequest.draw = Request.Form["draw"].FirstOrDefault();
            filtersFromRequest.start = Request.Form["start"].FirstOrDefault();
            filtersFromRequest.length = Request.Form["length"].FirstOrDefault();
            filtersFromRequest.sortColumn = Request.Form["columns[" + Request.Form["order[0][column]"].FirstOrDefault() + "][name]"].FirstOrDefault();
            filtersFromRequest.sortColumnDirection = Request.Form["order[0][dir]"].FirstOrDefault();
            filtersFromRequest.searchValue = Request.Form["search[value]"].FirstOrDefault();
            filtersFromRequest.pageSize = filtersFromRequest.length != null ? Convert.ToInt32(filtersFromRequest.length) : 0;
            filtersFromRequest.skip = filtersFromRequest.start != null ? Convert.ToInt32(filtersFromRequest.start) : 0;
            filtersFromRequest.sortColumnIndex = Request.Form["order[0][column]"].FirstOrDefault();

            filtersFromRequest.searchValue = filtersFromRequest.searchValue?.ToLower();
        }
8- نحوه‌ی استفاده از این متد در handler یا action مورد نظر:
Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);

9- با استفاده از متد زیر، مقادیر مورد نیاز دیتاتیبل را به آن ارسال می‌کنیم:
 public static PaginationDataTableResult<T> ToDataTableJs<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
        {
            int recordsTotal = source.Count();
            CofingPaging(ref filtersFromRequest, recordsTotal);
            var result = new PaginationDataTableResult<T>()
            {
                draw = filtersFromRequest.draw,
                recordsFiltered = recordsTotal,
                recordsTotal = recordsTotal,
                data = source.OrderByIndex(filtersFromRequest).Skip(filtersFromRequest.skip).Take(filtersFromRequest.pageSize).ToList()
            };

            return result;
        }

        private static void CofingPaging(ref FiltersFromRequestDataTable filtersFromRequest, int recordsTotal)
        {
            if (filtersFromRequest.pageSize == -1)
            {
                filtersFromRequest.pageSize = recordsTotal;
                filtersFromRequest.skip = 0;
            }
        }
private static IEnumerable<T> OrderByIndex<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest)
        {
            var props = typeof(T).GetProperties();
            string propertyName = "";
            for (int i = 0; i < props.Length; i++)
            {
                if (i.ToString() == filtersFromRequest.sortColumnIndex)
                    propertyName = props[i].Name;
            }

            System.Reflection.PropertyInfo propByName = typeof(T).GetProperty(propertyName);
            if (propByName is not null)
            {
                if (filtersFromRequest.sortColumnDirection == "desc")
                    source = source.OrderByDescending(x => propByName.GetValue(x, null));
                else
                    source = source.OrderBy(x => propByName.GetValue(x, null));
            }

            return source;
        }
که باهر دیتاتایپی کار می‌کند و خودش به صورت خودکار، عملیات مرتب‌سازی را انجام می‌دهد (ابدایی خودم)
10- نحوه استفاده در هندلر یا اکشن:
var result =  query.ToDataTableJs(filtersFromRequest);
return new JsonResult(result);
یک نکته: پراپرتی‌های شما باید باحروف کوچک باشد وگرنه در سمت جاوااسکریپت، خطای undefined را مشاهده خواهید کرد. در این حالت باید پراپرتی‌ها را با حروف کوچک شروع کنید؛ ولی اگر دارید با کتابخانه‌ی newtonSoft  و jsonCovert سریالایز میکنید، میتوانید از این attribute بالای پراپرتی‌ها استفاده کنید: [JsonProperty("name")]
درکل باید یک iqueryrable را آماده و به متد ToDataTableJs ارسال کنید.

- برای سرچ هم در column‌ها هم  میتوانید به شکل زیر عمل کنید.
ابتدا دو متد زیر را به یک کلاس static اضافه کنید:
 public static IEnumerable<TSource> WhereSearchValue<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
        {
            return source.Where(predicate);
        }
        public static bool ContainsSearchValue(this string source, string toCheck)
        {
            return source != null && toCheck != null && source.IndexOf(toCheck, StringComparison.OrdinalIgnoreCase) >= 0;
        }
بعد به این شکل از آن‌ها بین ایجاد iqueryrable  و جایی که متد todatatableJs فراخوانی می‌شود، استفاده کنید:
if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
برای افزایش کارآیی بهتر است مدل اصلی را به ویوو ارسال نکنید و از همان اول یک IQueryrable از جنس ویوومدل یا dto داشته باشید و این سرچ را هم بر روی همان انجام دهید.

کد کامل هندلر یا action  (که ترکیب کد‌های بالا هستش):
 public JsonResult OnPostList()
        {
            Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);
            var query = _Repo.GetQueryable().Select(x => new VmAdminList()
            {
                title = x.Title,
            }
            );

            if (!string.IsNullOrEmpty(filtersFromRequest.searchValue))
                query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();

            var result =  query.ToDataTableJs(filtersFromRequest);
            return new JsonResult(result);
        }

چند نکته:
1- ممکن‌است که بخواهید یکسری فیلتر را بجز مقادیر پیش فرض به سمت سرور ارسال کنید. برای اینکار کد زیر را به قسمت Ajax  فرانت‌اند اضافه کنید:
 data: function (d) {
                    d.parentId = parentID;
                    d.StartDateTime= StartDateTime;
                },
 و آن‌ها را به این شکل در سمت سرور دریافت کنید:
if (!int.TryParse(Request.Form["parentId"].FirstOrDefault(), out int parentId))
                throw new NullReferenceException();
2- در کانفیگ‌های Ajax مربوط به دیتاتیبل، دیگر کلید Success را نداریم؛ ولی به این شکل میتوانید این قسمت را شبیه سازی کنید:
 dataSrc: function (json) {
                    $("#count").val(json.data.length);
                    var sum = 0;
                    json.data.forEach(function (item) {
                        if (!isNullOrEmpty(item.credit))
                            sum += parseInt(item.credit);
                    })
                    $("#sum").val(separate(sum));

                    return json.data;
                }
که return آن الزامی هست؛ وگرنه به خطا میخورید.

تا به اینجا کار کاملا تمام شده؛ ولی من برای داینامیک کردن schema و column‌ها هم کلاسی را نوشته‌ام که فکر میکنم کار را راحت‌تر کند. چون شما برای تعداد ستون‌ها باید یک آبجکت را به شکل زیر تعریف کنید:
columns: [
        { data: 'name' },
        { data: 'position' },
        { data: 'salary' },
        { data: 'office' }
    ]
در اینجا اگر کلید‌ها و یا ستون‌ها (<th>) جابجا باشند، خطا می‌دهد و توسعه را بعدا سخت می‌کند؛ چون بعد هر بار تغییر، باید دستی این آبجکت‌ها و ستون‌ها را هم جابجا کنید. ولی با استفاده از کد‌های زیر، خودش به صورت داینامیک تولید می‌شود. کدزیر این کار رو انجام می‌دهد:
public class JsDataTblGeneretaor<T>
    {
        public readonly DataTableSchemaResult DataTableSchemaResult = new();
        public JsDataTblGeneretaor<T> CreateTableSchema()
        {
            var props = typeof(T).GetProperties();

            foreach (var prop in props)
            {
                DataTableSchemaResult.SchemaResult.Add(new()
                {
                    data = prop.Name,
                    sortable = (prop.PropertyType == typeof(int)) || (prop.PropertyType == typeof(bool)) || (prop.PropertyType == typeof(DateTime)),
                    width = "",
                    visible = (prop.PropertyType != typeof(DateTime))
                });
            }

            return this;
        }

        public JsDataTblGeneretaor<T> CreateTableColumns()
        {
            var props = typeof(T).GetProperties();

            CustomAttributeData displayAttribute;

            foreach (var prop in props)
            {
                string displayName = prop.Name;
                displayAttribute = prop.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "DisplayAttribute");
                if (displayAttribute != null)
                {
                    displayName = displayAttribute.NamedArguments.FirstOrDefault().TypedValue.Value.ToString();
                }

                DataTableSchemaResult.Colums.Add(displayName);
            }

            return this;
        }

        public JsDataTblGeneretaor<T> AddCustomSchema(string data, bool? sortable = null, bool? visible = null, string width = null, string className = null)
        {
            if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
                return this;

            foreach (var item in DataTableSchemaResult.SchemaResult.Where(x => x.data == data))
            {
                if (sortable != null)
                    item.sortable = sortable.Value;

                if (visible != null)
                    item.visible = visible.Value;

                if (width != null)
                    item.width = width;

                if (className != null)
                    item.className = className;
            }

            return this;
        }
        public JsDataTblGeneretaor<T> SerializeSchema()
        {
            if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any())
                return this;

            DataTableSchemaResult.SerializedSchemaResult = JsonSerializer.Serialize(DataTableSchemaResult.SchemaResult);

            return this;
        }
    }
    public class DataTableSchema
    {
        public string data { get; set; }
        public bool sortable { get; set; }
        public string width { get; set; }
        public bool visible { get; set; }
        public string className { get; set; }
    }
    public class DataTableSchemaResult
    {
        public readonly List<DataTableSchema> SchemaResult = new();
        public readonly List<string> Colums = new();
        public string SerializedSchemaResult = "";
    }
متد CreateTableSchema، آبجکت هایی را که دیتاتیبل نیاز دارد، ایجاد می‌کند (فرقی ندارد مدلت شما از چه جنسی باشد) که شامل یک لیست از آبجکت‌هاست و شما میتوانید بااستفاده از متد AddCustomSchema، آن را سفارشی سازی کنید؛ مثلا بگوئید فلان کلید نمایش داده نشود و یا عرضش را مشخص کنید و ...  
متد CreateTableColumns  خیلی ساده هست و فقط یک لیست از استرینگ‌ها را برمیگرداند.
SerializeSchema هم که لیست آبجکت‌های مورد نیاز دیتاتیبل را سریالایز می‌کند.

نحوه استفاده:
در متد آغازین برنامه باید این کلاس را صدا بزنید و با هر روشی که دوست دارید، به view یا razor page ارسال کنید:
public void OnGet()
        {
            //Create Data Table Js Schema and Columns Dynamicly
            JsDataTblGeneretaor<yourVM> tblGeneretaor = new();
            DataTableSchemaResult = tblGeneretaor.CreateTableColumns().CreateTableSchema().SerializeSchema().DataTableSchemaResult;
        }

نحوه سفارشی سازی:
.AddCustomSchema("yourProperty",visible:false)
که به سمت View ارسال می‌شودو حالا نحوه‌ی استفاده کردن از scheme ساخته شده:
var scheme = JSON.parse('@Html.Raw(Model.DataTableSchemaResult.SerializedSchemaResult)')
و استفاده‌ی از آن در گزینه‌های دیتاتیبل  columns: scheme,

نحوه ساخت ستون‌ها در view:
@foreach (var col in Model.DataTableSchemaResult.Colums)
              {
                <th>@col</th>
              }

نظرات مطالب
ASP.NET MVC #1
ASP.NET Web API وابستگی به ASP.NET MVC ندارد و در برنامه‌های وب فرم هم قابل استفاده‌است (یا حتی در یک سرویس ویندوز NT). در برنامه‌های ASP.NET MVC فعلی امکان استفاده‌ی از امکانات آن در Viewهای برنامه وجود دارد. این جزئیات در مطالب «گروه ASP.NET Web API» بیشتر بررسی شده‌اند.
نظرات مطالب
ASP.NET MVC #1
- مطالب «گروه ASP.NET Web API» را در سایت پیگیری کنید.
- دو گروه مجزا هستند. ASP.NET Web API جایگزین مدرنی است برای WCF Rest Services. در ASP.NET MVC 6 این دو با هم یکی خواهند شد و در یک کنترلر امکان استفاده از امکانات متداول ASP.NET MVC به همراه متدهای Web API میسر خواهد شد.
نظرات مطالب
با ASP.MVC چه مزایایی را به دست خواهیم آورد
سلام. تشکر از مقالات خوبتون. میخواستم بپرسم برای من که بیش از 2 سالی هست با سی شارپ و Win App کار کردم و میخوام برنامه نویسی تحت وب رو از صفر کار کنم کدوم یک بهتره؟ اول از ASP.NET شروع کنم و بعد برم سراغ ASP.NET MVC یا نه مستقیما از ASP.NET MVC شروع کنم. البته به صورت مقدماتی با ASP.NET آشنا هستم. ممنون میشم راهنمایی کنید.

مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت نهم - تعریف سرویس‌های Open Generics
فرض کنید در برنامه‌ی خود، یک سرویس جنریک را طراحی کرده‌اید. برای مثال خود ASP.NET Core به همراه سرویس جنریک <ILogger<T است و اگر برای نمونه بخواهیم آن‌را در سازنده‌ی کنترلری مانند ValuesController تزریق کنیم، نحوه‌ی تعریف آن به صورت <ILogger<ValuesController خواهد بود. هر چند تنظیمات این سرویس پیشتر انجام شده‌است، اما اگر بخواهیم آن‌را به همین نحو <ILogger<T به متدهایی مانند services.AddScoped معرفی کنیم، کار نمی‌کند؛ نمونه‌ی دیگری از این دست Generic Repositoryها هستند:
 // does not work: services.AddScoped<IGenericRepository<T>,EFRepository<T>>();


نحوه‌ی معرفی سرویس‌های جنریک نامحدود (Open Generics و یا Unbound Generics) به سیستم تزریق وابستگی‌ها

اگر بخواهیم یک سرویس جنریک را به سیستم تزریق وابستگی‌های برنامه‌های NET Core. به نحو متداولی معرفی کنیم، نیاز است به ازای تک تک Tهای میسر و تعریف شده‌ی در برنامه، اینکار صورت گیرد:
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IStore<User>, SqlStore<User>>();
    services.AddScoped<IStore<Invoice>, SqlStore<Invoice>>();
    services.AddScoped<IStore<Payment>, SqlStore<Payment>>();
    // ...
}
و در اینجا به ازای هر T یا موجودیت جدیدی در برنامه، نیاز است یک سطر دیگر را نیز تعریف کرد. خوشبختانه در این سیستم، امکان تعریف جنریک‌های نامحدود و باز نیز وجود دارد:
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(typeof(IStore<>), typeof(SqlStore<>));
}
به این ترتیب تمام سطرهایی که پیشتر تعریف کردیم، تبدیل به یک سطر فوق می‌شوند. در اینجا از Overload غیرجنریک متد AddScoped استفاده شده؛ به همین جهت از واژه‌ی کلیدی typeof برای معرفی نوع‌های جنریک باز کمک گرفته شده‌است. ذکر <> نیز به معنای تفسیر و وهله سازی هر نوع درخواستی رسیده، در زمان اجرا می‌باشد.


محدودیت کار کردن با جنریک‌های نامحدود در سیستم تزریق وابستگی‌ها

با تعریف تک سطر فوق، هر چند برنامه بدون مشکل کامپایل می‌شود، اما اگر در زمان اجرای برنامه، <IStore<T ای را درخواست کنید که میسر نباشد (در خواست هر نوعی در زمان اجرا با جنریک‌های باز معرفی شده، میسر است)، یک استثنای زمان اجرا را دریافت می‌کنید؛ برای مثال اگر نوع T به کلاس‌ها محدود شده باشد و در قسمتی از برنامه، <IStore<int درخواست شود. هرچند این موارد با یکبار آزمایش برنامه، قابل یافت شدن و رفع می‌باشند.


کتابخانه‌ی کمکی Scrutor نیز از جنریک‌های باز پشتیبانی می‌کند

در قسمت قبل نحوه‌ی اسکن اسمبلی‌های برنامه را توسط کتابخانه‌ی کمکی Scrutor بررسی کردیم. این کتابخانه امکان یافتن و فیلتر کلاس‌ها و معرفی آن‌ها را به سیستم تزریق وابستگی‌ها، بر اساس ویژگی جنریک‌های باز نیز دارا است:
services.Scan(scan => scan 
  .FromAssemblyOf<CombinedService>()  
    .AddClasses(x=> x.AssignableTo(typeof(IOpenGeneric<>))) // Can close generic types 
    .AsMatchingInterface())


ساده سازی «مثال 2: وهله سازی در صورت نیاز وابستگی‌های یک سرویس به کمک Lazy loading» قسمت ششم با جنریک‌های نامحدود

در قسمت ششم نحوه‌ی تعریف پیشنیازهای وهله سازی به تاخیر افتاده را با استفاده از کلاس Lazy بررسی کردیم:
services.AddTransient<IOrderHandler, OrderHandlerLazy>();
services.AddTransient<IAccounting, Accounting>()
            .AddTransient(serviceProvider => new Lazy<IAccounting>(() => serviceProvider.GetRequiredService<IAccounting>()));
services.AddTransient<ISales, Sales>()
           .AddTransient(serviceProvider => new Lazy<ISales>(() => serviceProvider.GetRequiredService<ISales>()));
- در اینجا در ابتدا تمام سرویس‌ها (حتی آن‌هایی که قرار است به صورت Lazy استفاده شوند) یکبار به صورت متداولی معرفی می‌شوند.
- سپس سرویس‌هایی که قرار است به صورت Lazy نیز واکشی شوند، بار دیگر توسط روش factory registration با وهله سازی new Lazy از نوع سرویس مدنظر و فراهم آوردن پیاده سازی آن با استفاده از serviceProvider.GetRequiredService، مجددا معرفی خواهند شد.

اگر به شرط دوم دقت کنید،  <new Lazy<IAccounting و <new Lazy<ISales، دقیقا مانند همان سرویس‌های جنریک <IStore<User و <IStore<Invoice تعریف شده‌اند. یعنی نیاز است به ازای هر T ممکن در برنامه، یکبار <new Lazy<T را نیز به سیستم تزریق وابستگی‌ها معرفی کرد. بنابراین تمام این تعریف‌های اضافی را می‌توان با یک سطر جنریک نامحدود زیر جایگزین و خلاصه کرد:
services.AddTransient(typeof(Lazy<>), typeof(LazyFactory<>));
و نکته‌ی جالب آن، نحوه‌ی تعریف قسمت factory متدهایی مانند AddTransient در اینجا است که به صورت زیر قابل پیاده سازی است:
public class LazyFactory<T> : Lazy<T> where T : class
{
    public LazyFactory(IServiceProvider provider)
        : base(() => provider.GetRequiredService<T>())
    {
    }
}
مطالب
تبدیل html به pdf با کیفیت بالا

کتابخانه iTextSharp دارای کلاسی است به نام HTMLWorker که کار تبدیل عناصر HTML را به عناصر متناظر خودش، انجام می‌دهد. این کلاس در حال حاضر منسوخ شده درنظر گرفته می‌شود (اینطور توسط نویسندگان آن اعلام شده) و دیگر توسعه نخواهد یافت. بنابراین اگر از HTMLWorker استفاده می‌کنید با یک کلاس قدیمی که دارای HTML Parser ایی بسیار بدوی است طرف هستید و در کل برای تبدیل محتوای HTML ایی با ساختار بسیار ساده بد نیست؛ اما انتظار زیادی از آن نداشته باشید.
جایگزین کلاس HTMLWorker در این کتابخانه در حال حاضر کتابخانه itextsharp.xmlworker است، که به صورت یک افزونه در کنار کتابخانه اصلی در حال توسعه می‌باشد. مشکل اصلی این کتابخانه، عدم پشتیبانی از UTF8 و راست به چپ است. بنابراین حداقل به درد کار ما نمی‌خورد.

راه حل بسیار بهتری برای موضوع اصلی بحث ما وجود دارد و آن هم استفاده از موتور WebKit (همان موتوری که برای مثال در Apple Safari استفاده می‌شود) برای HTML parsing و سپس تبدیل نتیجه نهایی به PDF است. پروژه‌ای که این مقصود را میسر کرده، wkhtmltopdf نام دارد.
توسط آن به کمک موتور WebKit، کار HTML Parsing انجام شده و سپس برای تبدیل عناصر نهایی به PDF از امکانات کتابخانه‌ای به نام QT استفاده می‌شود. کیفیت نهایی آن کپی مطابق اصل HTML قابل مشاهده در یک مرورگر است و با یونیکد و زبان فارسی هم مشکلی ندارد.

برای استفاده از این کتابخانه‌ی native در دات نت، شخصی پروژه‌ای را ایجاد کرده است به نام WkHtmlToXSharp که محصور کننده‌ی wkhtmltopdf می‌باشد. در ادامه به نحوه استفاده از آن خواهیم پرداخت:

الف) دریافت پروژه WkHtmlToXSharp
پروژه WkHtmlToXSharp را از آدرس زیر می‌توانید دریافت کنید.

 این پروژه به همراه فایل‌های کامپایل شده نهایی wkhtmltopdf نیز می‌باشد و حجمی حدود 40 مگ دارد. به علاوه فعلا نسخه 32 بیتی آن در دسترس است. بنابراین باید دقت داشت که نباید تنظیمات پروژه دات نت خود را بر روی Any CPU قرار دهیم، زیرا در این حالت برنامه شما در یک سیستم 64 بیتی بلافاصله کرش خواهد کرد. تنظیمات target platform پروژه دات نتی ما حتما باید بر روی X86 تنظیم شود.

ب) پس از دریافت این پروژه و افزودن ارجاعی به اسمبلی WkHtmlToXSharp.dll، استفاده از آن به نحو زیر می‌باشد:

using System.IO;
using WkHtmlToXSharp;
using System;

namespace Test2
{
    public class WkHtmlToXSharpTest
    {
        public static void ConvertHtmlStringToPdfTest()
        {
            using (var wk = new MultiplexingConverter())
            {
                wk.Begin += (s, e) => Console.WriteLine("Conversion begin, phase count: {0}", e.Value);
                wk.Error += (s, e) => Console.WriteLine(e.Value);
                wk.Warning += (s, e) => Console.WriteLine(e.Value);
                wk.PhaseChanged += (s, e) => Console.WriteLine("PhaseChanged: {0} - {1}", e.Value, e.Value2);
                wk.ProgressChanged += (s, e) => Console.WriteLine("ProgressChanged: {0} - {1}", e.Value, e.Value2);
                wk.Finished += (s, e) => Console.WriteLine("Finished: {0}", e.Value ? "success" : "failed!");

                wk.GlobalSettings.Margin.Top = "0cm";
                wk.GlobalSettings.Margin.Bottom = "0cm";
                wk.GlobalSettings.Margin.Left = "0cm";
                wk.GlobalSettings.Margin.Right = "0cm";

                wk.ObjectSettings.Web.EnablePlugins = false;
                wk.ObjectSettings.Web.EnableJavascript = false;
                wk.ObjectSettings.Load.Proxy = "none";

                var htmlString = File.ReadAllText(@"c:\page.xhtml");
                var tmp = wk.Convert(htmlString);

                File.WriteAllBytes(@"tst.pdf", tmp);
            }
        }
    }
}

کار با وهله سازی از کلاس MultiplexingConverter شروع می‌شود. اگر علاقمند باشید که درصد پیشرفت کار به همراه خطاهای احتمالی پردازشی را ملاحظه کنید می‌توان از رخدادگردان‌هایی مانند ProgressChanged و Error استفاده نمائید که نمونه‌ای از آن در کد فوق بکارگرفته شده است.
تبدیل HTML به PDF آنچنان تنظیمات خاصی ندارد زیرا فرض بر این است که قرار است از همان تنظیمات اصلی HTML مورد نظر استفاده گردد. اما اگر نیاز به تنظیمات بیشتری وجود داشت، برای مثال به کمک GlobalSettings آن می‌توان حاشیه‌های صفحات فایل نهایی تولیدی را تنظیم کرد.
موتور WebKit با توجه به اینکه موتور یک مرورگر است، امکان پردازش جاوا اسکریپت را هم دارد. بنابراین اگر قصد استفاده از آن‌را نداشتید می‌توان خاصیت ObjectSettings.Web.EnableJavascript را به false مقدار دهی کرد.
کار اصلی، در متد Convert انجام می‌شود. در اینجا می‌توان یک رشته را که حاوی فایل HTML مورد نظر است به آن ارسال کرد و نتیجه نهایی، آرایه‌ای از بایت‌ها، حاوی فایل باینری PDF تولیدی است.
روش دیگر استفاده از این کتابخانه، مقدار دهی wk.ObjectSettings.Page می‌باشد. در اینجا می‌توان Url یک صفحه اینترنتی را مشخص ساخت. در این حالت دیگر نیازی نیست تا به متد Convert پارامتری را ارسال کرد. می‌توان از overload بدون پارامتر آن استفاده نمود.

یک نکته:
اگر می‌خواهید زبان فارسی را توسط این کتابخانه به درستی پردازش کنید، نیاز است حتما یک سطر زیر را به header فایل html خود اضافه نمائید:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

 
مسیرراه‌ها
WPF
          مطالب
          استفاده از افزونه MD.BootstrapPersianDateTimePicker در گریدهای Kendo UI
          در این مطلب قصد داریم نحوه‌ی یکپارچه سازی افزونه‌ی انتخاب تاریخ و زمان MD.BootstrapPersianDateTimePicker را با گریدهای Kendo UI، در دو حالت ویرایش به صورت popup و یا inline، بررسی کنیم:



          پیشنیازها

          برای این مطلب از دو کتابخانه‌ی moment-jalaali، برای تبدیل تاریخ، از میلادی به شمسی و برعکس، استفاده خواهیم کرد. همچنین کنترل انتخاب تاریخ نیز از کتابخانه‌ی MD.BootstrapPersianDateTimePicker تامین می‌شود.
          moment-jalaali را می‌توانید به صورت ذیل از طریق npm نصب کنید:
           npm install moment-jalaali --save
          سپس دو مدخل ذیل را باید به مجموعه‌ی مداخل اسکریپت‌های صفحه‌ی خود اضافه کنید:
          node_modules/moment/min/moment.min.js
          node_modules/moment-jalaali/build/moment-jalaali.js

          MD.BootstrapPersianDateTimePicker را یا از طریق نیوگت نصب کنید و یا مخزن کد آن‌را به صورت کامل دریافت کنید:
           Install-Package MD.BootstrapPersianDateTimePicker
          پس از دریافت این بسته، فایل jquery.Bootstrap-PersianDateTimePicker.css آن‌را به مداخل cssهای خود اضافه کنید.
          همچنین فایل‌های jalaali.js و jquery.Bootstrap-PersianDateTimePicker.js آن‌را نیز به مداخل اسکریپت‌های صفحه‌ی خود اضافه نمائید.

          در کل اگر از ASP.NET Core استفاده می‌کنید، فایل bundleconfig.json یک چنین شکلی را پیدا خواهد کرد. در این حالت فایل layout برنامه تنها این دو مدخل نهایی را نیاز خواهد داشت:
          <link href="~/css/site.min.css" rel="stylesheet" asp-append-version="true" />
          <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>


          فعالسازی کنترل انتخاب تاریخ شمسی بجای کامپوننت پیش فرض انتخاب تاریخ Kendo UI

          پس از افزودن ارجاعات مورد نیاز، اکنون فرض ما بر این است که ستون تاریخ، دقیقا با فرمت میلادی از سمت سرور دریافت می‌شود و addDate نام دارد.
          پس از آن، مرحله‌ی اول کار، نمایش این تاریخ میلادی به صورت شمسی است:
          {
             field: "addDate", title: "تاریخ ثبت",
             template: "#=moment(addDate).format('jYYYY/jMM/jDD')#",
          این‌کار را با تعریف یک template و سپس ارسال مقدار addDate که همان نام خاصیت ستون جاری است به کتابخانه‌ی moment-jalaali، انجام می‌دهیم. در این حالت همواره تاریخ‌های میلادی تنظیم شده‌ی در این ستون، در حالت نمایش معمولی با فرمت شمسی ظاهر می‌شوند.

          در ادامه‌ی تکمیل خواص ستون جاری، خاصیت editor را اضافه خواهیم کرد:
           editor: function(container, options) {
          }
          توسط پارامتر اول آن می‌توان کار سفارشی سازی کنترل ویرایش این ستون را انجام داد و خاصیت دوم آن، نام فیلد جاری و همچنین مقادیر مدل آن‌را در اختیار ما قرار می‌دهد.
          در ادامه نیاز است یک input field سفارشی را در اینجا درج کنیم تا کار نمایش کنترل انتخاب تاریخ شمسی را انجام دهد:
          // دریافت تاریخ میلادی و تبدیل آن به شمسی جهت نمایش در تکست باکس
          var persianAddDate = moment(options.model.addDate).format('jYYYY/jMM/jDD');
          // ایجاد کنترل انتخاب تاریخ سفارشی با مقدار تاریخ شمسی دریف جاری
          var input = $('<div dir="ltr" class="input-group">'+
          '<div class="input-group-addon" data-name="datepicker1" data-mddatetimepicker="true" data-trigger="click" data-targetselector="#' + options.field + '" data-fromdate="false" data-enabletimepicker="false" data-englishnumber="true" data-placement="left">'+
          '<span class="glyphicon glyphicon-calendar"></span>'+
          '</div>'+
          '<input type="text" value="'+ persianAddDate +'" class="form-control" id="' + options.field + '" placeholder="از تاریخ" data-mddatetimepicker="true" data-trigger="click" data-targetselector="#' + options.field + '" data-englishnumber="true" data-fromdate="true" data-enabletimepicker="false" data-placement="right" />'+
          '</div>');
          // افزودن کنترل جدید به صفحه
          input.appendTo(container);
          کاری که در اینجا انجام شده، افزودن یک input مزین شده با ویژگی‌های مخصوص MD.BootstrapPersianDateTimePicker است تا توسط آن شناسایی شود. همچنین چون مقدار options.model.addDate در اصل میلادی است، توسط کتابخانه‌ی moment-jalaali به شمسی تبدیل و بجای value این کنترل ارائه می‌شود تا زمانیکه این ستون در حالت ویرایش قرار گرفت، این تاریخ شمسی توسط کنترل انتخاب تاریخ، به درستی پردازش شده و نمایش داده شود.
          متد input.appendTo، کار افزودن این input جدید را به container یا همان محل نمایش ستون جاری، انجام می‌دهد.

          در این حالت اگر برنامه را اجرا کنید، هرچند ظاهر Input جدید تغییر کرده‌است، اما سبب نمایش کنترل انتخاب تاریخ نمی‌شود؛ چون این فیلد ویرایشی، پس از رندر صفحه، به صفحه اضافه شده‌است. به همین جهت نیاز است متد EnableMdDateTimePickers نیز فراخوانی شود. این متد کار فعالسازی input جدید را انجام خواهد داد:
           // با خبر سازی کتابخانه انتخاب تاریخ از تکست باکس جدید
          EnableMdDateTimePickers();

          تا اینجا موفق شدیم بجای کنترل انتخاب تاریخ میلادی، کنترل انتخاب تاریخ شمسی را نمایش دهیم. اما تاریخی که انتخاب می‌شود نیز شمسی است و تاریخی که به سمت سرور ارسال خواهد شد، میلادی می‌باشد. به همین جهت تغییرات این کنترل را تحت نظر قرار داده و هر زمانیکه کاربر تاریخی را انتخاب کرد، آن‌را به میلادی تبدیل کرده و بجای فیلد addDate اصلی تنظیم می‌کنیم:
          // هر زمانیکه کاربر تاریخ جدیدی را وارد کرد، آن‌را به میلادی تبدیل کرده و در مدل ردیف جاری ثبت می‌کنیم
          // در نهایت این مقدار میلادی است که به سمت سرور ارسال خواهد شد
          $('#' + options.field).change(function(){
             var selectedPersianDate = $(this)[0].value;
             var gregorianAddDate = moment(selectedPersianDate, 'jYYYY/jMM/jDD').format('YYYY-MM-DD');
             options.model.set('addDate', gregorianAddDate);
          });

          تنها مرحله‌ی باقیمانده، مخفی کردن این کنترل نمایش تاریخ، با از دست رفتن فوکوس است:
          // با از دست رفتن فوکوس نیاز است این کنترل مخفی شود
          $('#' + options.field).blur(function(){
             $('[data-name="datepicker1"]').MdPersianDateTimePicker('hide');
          });
          اگر این کار را انجام ندهیم، در حالت popup با بسته شدن آن، هنوز کنترل نمایش تاریخ نمایان خواهد بود و بسته نمی‌شود یا در حالت ویرایش inline، با کلیک بر روی دکمه‌ی لغو ویرایش ردیف، باز هم شاهد باقی ماندن این کنترل در صفحه خواهیم بود که تجربه‌ی کاربری مطلوبی به شمار نمی‌رود.


          کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: با این کنترلر و این View