ایندکسها در RavenDB
خوب؛ اکنون این سؤال مطرح میشود که RavenDB چگونه اطلاعاتی را در این اسناد بدون اسکیما جستجو میکند؟ اینجا است که مفهوم و کاربرد ایندکسها مطرح میشوند. ما در قسمت قبل که کوئری نویسی مقدماتی را بررسی کردیم، عملا ایندکس خاصی را به صورت دستی جهت انجام جستجوها ایجاد نکردیم؛ از این جهت که خود RavenDB به کمک امکانات dynamic indexing آن، پیشتر اینکار را انجام داده است. برای نمونه به سطر ارسال کوئری به سرور، که در قسمت قبل ارائه شد، دقت کنید. در اینجا ارسال کوئری به indexes/dynamic کاملا مشخص است:
Request # 2: GET - 3,818 ms - <system> - 200 - /indexes/dynamic/Questions?&query=Title%3ARaven*&pageSize=128
Dynamic Indexes یا ایندکسهای پویا
ایندکسهای پویا زمانی ایجاد خواهند شد که ایندکس صریحی توسط برنامه نویس تعریف نگردد. برای مثال زمانیکه یک کوئری LINQ را صادر میکنیم، RavenDB بر این اساس و برای مثال فیلدهای قسمت Where آن، ایندکس پویایی را تولید خواهد کرد. ایجاد ایندکسها در RavenDB از اصل عاقبت یک دست شدن پیروی میکنند. یعنی مدتی طول خواهد کشید تا کل اطلاعات بر اساس ایندکس جدیدی که در حال تهیه است، ایندکس شوند. بنابراین تولید ایندکسهای پویا در زمان اولین بار اجرای کوئری، کوئری اول را اندکی کند جلوه خواهند داد؛ اما کوئریهای بعدی که بر روی یک ایندکس آماده اجرا میشوند، بسیار سریع خواهند بود.
Static indexes یا ایندکسهای ایستا
ایندکسهای پویا به دلیل وقفه ابتدایی که برای تولید آنها وجود خواهد داشت، شاید آنچنان مطلوب به نظر نرسند. اینجا است که مفهوم ایندکسهای ایستا مطرح میشوند. در این حالت ما به RavenDB خواهیم گفت که چه چیزی را ایندکس کند. برای تولید ایندکسهای ایستا، از مفاهیم Map/Reduce که در پیشنیازهای دوره جاری در مورد آن بحث شد، استفاده میگردد. خوشبختانه تهیه Map/Reduceها در RavenDB پیچیده نبوده و کل عملیات آن توسط کوئریهای LINQ قابل پیاده سازی است.
تهیه ایندکسهای پویا نیز در تردهای پسزمینه انجام میشوند. از آنجائیکه RavenDB برای اعمال Read، بهینه سازی شده است، با ارسال یک کوئری به آن، این بانک اطلاعاتی، کلیه اطلاعات آماده را در اختیار شما قرار خواهد داد؛ صرفنظر از اینکه کار تهیه ایندکس تمام شده است یا خیر.
چگونه یک ایندکس ایستا را ایجاد کنیم؟
اگر به کنسول مدیریتی سیلورلایت RavenDB مراجعه کنیم، حاصل کوئریهای LINQ قسمت قبل را در برگهی ایندکسهای آن میتوان مشاهده کرد:
در اینجا بر روی دکمه Edit کلیک نمائید، تا با نحوه تهیه این ایندکس پویا آشنا شویم:
این ایندکس، یک نام داشته به همراه قسمت Map از پروسه Map/Reduce که توسط یک کوئری LINQ تهیه شده است. کاری که در اینجا انجام شده، ایندکس کردن کلیه سؤالات، بر اساس خاصیت عنوان آنها است.
اکنون اگر بخواهیم همین کار را با کدنویسی انجام دهیم، به صورت زیر میتوان عمل کرد:
using System; using System.Linq; using Raven.Client.Document; using RavenDBSample01.Models; using Raven.Client; using Raven.Client.Linq; using Raven.Client.Indexes; namespace RavenDBSample01 { class Program { static void Main(string[] args) { using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { store.DatabaseCommands.PutIndex( name: "Questions/ByTitle", indexDef: new IndexDefinitionBuilder<Question> { Map = questions => questions.Select(question => new { Title = question.Title } ) }); } } } }
برنامه را اجرا کرده و سپس به کنسول مدیریتی تحت وب RavenDB، قسمت ایندکسهای آن مراجعه کنید. در اینجا میتوان ایندکس جدید ایجاد شده را مشاهده کرد:
هرچند همین اعمال را در کنسول مدیریتی نیز میتوان انجام داد، اما مزیت آن در سمت کدها، دسترسی به intellisense و نوشتن کوئریهای strongly typed است.
روش استفاده از store.DatabaseCommands.PutIndex اولین روش تولید Index در RavenDB با کدنویسی است. روش دوم، بر اساس ارث بری از کلاس AbstractIndexCreationTask شروع میشود و مناسب است برای حالتیکه نمیخواهید کدهای تولید ایندکس، با کدهای سایر قسمتهای برنامه مخلوط شوند:
public class QuestionsByTitle : AbstractIndexCreationTask<Question> { public QuestionsByTitle() { Map = questions => questions.Select(question => new { Title = question.Title }); } }
اکنون برای معرفی آن به برنامه باید از متد IndexCreation.CreateIndexes استفاده کرد. این متد، نیاز به دریافت اسمبلی محل تعریف کلاسهای تولید ایندکس را دارد. به این ترتیب تمام کلاسهای مشتق شده از AbstractIndexCreationTask را یافته و ایندکسهای متناظری را تولید میکند.
using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { IndexCreation.CreateIndexes(typeof(QuestionsByTitle).Assembly, store); }
استفاده از ایندکسهای ایستای ایجاد شده
تا اینجا موفق شدیم ایندکسهای ایستای خود را با کد نویسی ایجاد کنیم. در ادامه قصد داریم از این ایندکسها در کوئریهای خود استفاده نمائیم.
using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { var questions = session.Query<Question>(indexName: "QuestionsByTitle") .Where(x => x.Title.StartsWith("Raven")).Take(128); foreach (var question in questions) { Console.WriteLine(question.Title); } } }
Request # 147: GET - 58 ms - <system> - 200 - /indexes/QuestionsByTitle?&query=Title%3ARaven*&pageSize=128 Query: Title:Raven* Time: 7 ms Index: QuestionsByTitle Results: 2 returned out of 2 total.
var questions = session.Query<Question, QuestionsByTitle>() .Where(x => x.Title.StartsWith("Raven")).Take(128);
ایجاد ایندکسهای پیشرفته با پیاده سازی Map/Reduce
حالتی را در نظر بگیرید که در آن قصد داریم تعداد عنوانهای سؤالات مانند هم را بیابیم (یا تعداد مطالب گروههای مختلف یک وبلاگ را محاسبه کنیم). برای انجام اینکار با سرعت بسیار بالا، میتوانیم از ایندکسهایی با قابلیت محاسباتی در RavenDB استفاده کنیم. کار با ارث بری از کلاس AbstractIndexCreationTask شروع میشود. آرگومان جنریک اول آن، نام کلاسی است که در تهیه ایندکس شرکت خواهد داشت و آرگومان دوم (و اختیاری) ذکر شده، نتیجه عملیات Reduce است:
public class QuestionsCountByTitleReduceResult { public string Title { set; get; } public int Count { set; get; } } public class QuestionsCountByTitle : AbstractIndexCreationTask<Question, QuestionsCountByTitleReduceResult> { public QuestionsCountByTitle() { Map = questions => questions.Select(question => new { Title = question.Title, Count = 1 }); Reduce = results => results.GroupBy(x => x.Title) .Select(g => new { Title = g.Key, Count = g.Sum(x => x.Count) }); } }
اکنون برای استفاده از این ایندکس، ابتدا توسط متد IndexCreation.CreateIndexes، کار معرفی آن به RavenDB صورت گرفته و سپس متد Query سشن باز شده، دو آرگومان جنریگ را خواهد پذیرفت. اولین آرگومان، همان نتیجه Map/Reduce است و دومین آرگومان نام کلاس ایندکس جدید تعریف شده میباشد:
using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { IndexCreation.CreateIndexes(typeof(QuestionsCountByTitle).Assembly, store); using (var session = store.OpenSession()) { var result = session.Query<QuestionsCountByTitleReduceResult, QuestionsCountByTitle>() .FirstOrDefault(x => x.Title == "Raven") ?? new QuestionsCountByTitleReduceResult(); Console.WriteLine(result.Count); } }
در مواقعی نیاز است تا محتوایی را به صورت داینامیک به کدهای موجود در یک app Angular تزریق کنیم و یا محتوای موجود را تغییر دهیم. اگر این محتوای لود شده دارای دایرکتیوهای Angularباشد بعد از لود محتوا متوجه میشویم که کدهای Angular ما کار نمیکنند. چرا؟ اینجاست که نقش compile$ مشخص میشود. از طریق compile$ محتوای لود شده مجددا در محدوده تحت مدیریت Angular قرار گرفته و مشکل حل میشود. مثال زیر را بررسی میکنیم :
app.directive("otcDynamic", function(){ return { link: function(scope, element){ element.html("<button ng-click='doSomething()'>{{label}}</button>"); } }; });
در کد فوق ما یک دکمه را که در کلیک آن قرار است متدی توسط Angular فراخوانی شود، به صورت داینامیک ایجاد کرده و به المنت مورد نظرمان اضافه کردهایم. دقت داشته باشید که این محتوا ممکن است مثلا توسط درخواستهای Ajax از جایی فراخوانی شود. با کلیک بر روی دکمهی ایجاد شده، متد doSomething اجرا نخواهد شد؛ چرا که Angular قادر به تشخیص HTML تزریق شده نمیباشد و در واقع اصلا از وجود کد ما خبری ندارد. برای حل این مشکل از compile $ استفاده میکنیم:
app.directive("otcDynamic", function($compile){ return{ link: function(scope, element){ var template = "<button ng-click='doSomething()'>{{label}}</button>"; var linkFn = $compile(template); var content = linkFn(scope); element.append(content); } } });
با استفاده از compile $ در واقع ما
کدهای خود را مجددا تحت مدیریت Angular قرار میدهیم.
مثال فوق یک مثال ساده از این مبحث بود. در واقع کدهای تزریق شده میتوانند شامل کل
HTML تحت
مدیریت Angular
باشد که مثلا از طریق درخواستهای Ajax بهروز رسانی میشوند.
FluentValidation #2
NotNull | اطمینان از اینکه خاصیت مورد نظر Null نباشد |
NotEmpty | اطمینان از اینکه خاصیت مورد نظر Null یا رشته خالی نباشد (یا مقدار پیش فرض نباشد، مثلا 0 برای int) |
NotEqual | اطمینان از اینکه خاصیت مورد نظر برابر مقدار تعیین شده نباشد (یا برابر مقدار خاصیت دیگری نباشد) |
Equal | اطمینان از اینکه خاصیت مور نظر برابر مقدار تعیین شده باشد (یا برابر مقدار خاصیت دیگری باشد) |
Length | اطمینان از اینکه طول رشتهی خاصیت مورد نظر در محدوده خاصی باشد |
LessThan | اطمینان از اینکه مقدار خاصیت مورد نظر کوچکتر از مقدار تعیین شده باشد (یا کوچکتر از خاصیت دیگری) |
LessThanOrEqual | اطمینان از اینکه مقدار خاصیت مورد نظر کوچکتر یا مساوی مقدار تعیین شده باشد (یا کوچکتر مساوی مقدار خاصیت دیگری) |
GreaterThan | اطمینان از اینکه مقدار خاصیت مورد نظر بزرگتر از مقدار تعیین شده باشد (یا بزرگتر از مقدار خاصیت دیگری) |
GreaterThanOrEqual | اطمینان از اینکه مقدار خاصیت مورد نظر بزرگتر مساوی مقدار تعیین شده باشد (یا بزرگتر مساوی مقدار خاصیت دیگری) |
Matches | اطمینان از اینکه مقدار خاصیت مورد نظر با عبارت باقائده (Regular Expression) تنظیم شده مطابقت داشته باشد |
Must | اعتبارسنجی یک predicate با استفاده از Lambada Expressions. اگر عبارت Lambada مقدار true برگرداند اعتبارسنجی با موفقیت انجام شده و اگر false برگرداند، اعتبارسنجی با شکست مواجه شده است. |
اطمینان از اینکه مقدار خاصیت مورد نظر یک آدرس ایمیل معتبر باشد | |
CreditCard | اطمینان از اینکه مقدار خاصیت مورد نظر یک Credit Card باشد |
RuleFor(customer => customer.Surname).NotEqual(customer => customer.Forename);
RuleFor(customer => customer.Surname).NotNull().WithMessage("Please ensure that you have entered your Surname");
اعتبارسنجی تنها در مواقع خاص
RuleFor(customer => customer.CustomerDiscount).GreaterThan(0).When(customer => customer.IsPreferredCustomer);
When(customer => customer.IsPreferred, () => { RuleFor(customer => customer.CustomerDiscount).GreaterThan(0); RuleFor(customer => customer.CreditCardNumber).NotNull(); });
تعیین نحوه برخورد با اعتبارسنجیهای زنجیره ای
RuleFor(x => x.Surname).NotNull().NotEqual("foo");
RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo");
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { // First set the cascade mode CascadeMode = CascadeMode.StopOnFirstFailure; // Rule definitions follow RuleFor(...) RuleFor(...) } }
راه اول ایجاد یک کلاس که از PropertyValidator مشتق میشود. برای توضیح نحوه استفاده از این راه، تصور کنید که میخواهیم یک اعتبارسنج سفارشی درست کنیم تا چک کند که یک لیست حتماً کمتر از 10 آیتم داخل خود داشته باشد. در این صورت کدی که بایستی نوشته شود به صورت زیر خواهد بود:
using System.Collections.Generic; using FluentValidation.Validators; public class ListMustContainFewerThanTenItemsValidator<T> : PropertyValidator { public ListMustContainFewerThanTenItemsValidator() : base("Property {PropertyName} contains more than 10 items!") { } protected override bool IsValid(PropertyValidatorContext context) { var list = context.PropertyValue as IList<T>; if(list != null && list.Count >= 10) { return false; } return true; } }
برای استفاده از این Validator سفارشی نیز میتوان از متد SetValidator به صورت زیر استفاده نمود:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).SetValidator(new ListMustContainFewerThanTenItemsValidator<Pet>()); } }
راه دیگر استفاده از آن تعریف یک Extension Method میباشد که در این صورت میتوان از آن به صورت زنجیره ای مانند دیگر Validatorها استفاده نمود:
public static class MyValidatorExtensions { public static IRuleBuilderOptions<T, IList<TElement>> MustContainFewerThanTenItems<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder) { return ruleBuilder.SetValidator(new ListMustContainFewerThanTenItemsValidator<TElement>()); } }
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).MustContainFewerThanTenItems(); } }
راه دوم استفاده از متد Custom میباشد. برای توضیح نحوه استفاه از این متد مثال قبل (چک کردن تعداد آیتمهای لیست) را به صورت زیر بازنویسی میکنیم:
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { Custom(person => { return person.Pets.Count >= 10 ? new ValidationFailure("More than 10 pets is not allowed.") : null; }); } }
public class PersonValidator : AbstractValidator<Person> { public PersonValidator() { RuleFor(person => person.Pets).Must(HaveFewerThanTenPets).WithMessage("More than 9 pets is not allowed"); } private bool HaveFewerThanTenPets(IList<Pet> pets) { return pets.Count < 10; } }
پ.ن.
در این دو مقاله سعی شد تا ویژگیهای FluentValidation به صورت انتزاعی توضیح داده شود. در قسمت بعد نحوه استفاده از این کتابخانه در یک برنامه ASP.NET MVC نشان داده خواهد شد.
@Html.AntiForgeryToken()
//Post in Ajax services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
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, }); }
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" },
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; } }
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(); }
Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);
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; }
var result = query.ToDataTableJs(filtersFromRequest); return new JsonResult(result);
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; }
if (!string.IsNullOrEmpty(filtersFromRequest.searchValue)) query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
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); }
data: function (d) { d.parentId = parentID; d.StartDateTime= StartDateTime; },
if (!int.TryParse(Request.Form["parentId"].FirstOrDefault(), out int parentId)) throw new NullReferenceException();
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; }
columns: [ { data: 'name' }, { data: 'position' }, { data: 'salary' }, { data: 'office' } ]
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 = ""; }
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)
var scheme = JSON.parse('@Html.Raw(Model.DataTableSchemaResult.SerializedSchemaResult)')
@foreach (var col in Model.DataTableSchemaResult.Colums) { <th>@col</th> }
- میخواهیم footer پیش فرض PdfReport را که تاریخ را در یک سمت، و شماره صفحه را در سمتی دیگر نمایش میدهد، به عبارت «صفحه x از n» تغییر دهیم.
- میخواهیم در Header گزارش بجای Header پیش فرض PdfReport یکی از قالبهای PDF تهیه شده توسط Open Office را نمایش دهیم (و یا هر ساختار دیگری را).
تمام اجزای PdfReport جهت امکان اعمال تغییرات کلی و توسعه آنها طراحی شدهاند؛ قالبها، هدر، فوتر، منابع داده، قالبهای نمایش سلولها، تعریف توابع تجمعی سفارشی و غیره. جهت سهولت کار، به ازای هر یک از این موارد، پیاده سازیهای پیش فرضی در PdfReport قرار دارند، امکان اگر مورد رضایت شما نیستند ... از بنیان تغییرشان دهید! (و همچنین اگر مورد جالبی را پیاده سازی کردید، میتوانید به عنوان یک وصله جدید ارائه دهید تا به پروژه اضافه شود)
ضمنا این مطالب سفارشی سازی نیاز به آشنایی با ساختار iTextSharp را نیز دارند؛ در حد ایجاد یک جدول ساده باید با iTextSharp آشنا باشید.
مدلهای مورد استفاده:
namespace PdfReportSamples.Models { public class Task { public int Id { set; get; } public string Name { set; get; } public int PercentCompleted { set; get; } public bool IsActive { set; get; } public User Assignee { set; get; } } }
using System; namespace PdfReportSamples.Models { public class User { public int Id { set; get; } public string Name { set; get; } public string LastName { set; get; } public long Balance { set; get; } public DateTime RegisterDate { set; get; } } }
column.PropertyName<Task>(x => x.Assignee.Name)
using System; using System.Collections.Generic; using System.Drawing; using PdfReportSamples.Models; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.CustomHeaderFooter { public class CustomHeaderFooterPdfReport { readonly CustomHeader _customHeader = new CustomHeader(); public IPdfReportData CreatePdfReport() { return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.LeftToRight); 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(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight)); }) .PagesHeader(header => { header.CustomHeader(_customHeader); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.SilverTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.MultipleColumnsPerPage(new MultipleColumnsPerPage { ColumnsGap = 22, ColumnsPerPage = 2, ColumnsWidth = 250, IsRightToLeft = false, TopMargin = 7 }); }) .MainTableDataSource(dataSource => { var rows = new List<Task>(); var rnd = new Random(); for (int i = 1; i < 210; i++) { rows.Add(new Task { Assignee = new User { Id = i, Name = "user-" + i }, IsActive = rnd.Next(0, 2) == 1 ? true : false, Name = "task-" + i }); } dataSource.StronglyTypedList(rows); }) .MainTableColumns(columns => { columns.AddColumn(column => { column.PropertyName("rowNo"); column.IsRowNumber(true); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(0); column.Width(1); column.HeaderCell("#"); }); columns.AddColumn(column => { column.PropertyName<Task>(x => x.Name); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(1); column.Width(3); column.HeaderCell("Task Name"); }); columns.AddColumn(column => { column.PropertyName<Task>(x => x.Assignee.Name); // nested property support column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(3); column.HeaderCell("Assignee"); }); columns.AddColumn(column => { column.PropertyName<Task>(x => x.IsActive); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(2); column.HeaderCell("Active"); column.ColumnItemsTemplate(template => { template.Checkmark(checkmarkFillColor: Color.Green, crossSignFillColor: Color.DarkRed); }); }); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); }) .Export(export => { export.ToExcel(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\CustomHeaderFooterPdfReportSample.pdf")); } } }
به همراه Header سفارشی:
using System.Collections.Generic; using iTextSharp.text; using iTextSharp.text.pdf; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; namespace PdfReportSamples.CustomHeaderFooter { public class CustomHeader : IPageHeader { public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> rowdata, IList<SummaryCellData> summaryData) { return null; } Image _image; public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData) { if (_image == null) //cache is empty { var templatePath = AppPath.ApplicationPath + "\\data\\PdfHeaderTemplate.pdf"; _image = PdfImageHelper.GetITextSharpImageFromPdfTemplate(pdfWriter, templatePath); } var table = new PdfPTable(1); var cell = new PdfPCell(_image, true) { Border = 0 }; table.AddCell(cell); return table; } } }
و Footer سفارشی استفاده شده:
using System.Collections.Generic; using iTextSharp.text; using iTextSharp.text.pdf; using PdfRpt.Core.Contracts; namespace PdfReportSamples.CustomHeaderFooter { public class CustomFooter : IPageFooter { PdfContentByte _pdfContentByte; readonly IPdfFont _pdfRptFont; readonly Font _font; readonly PdfRunDirection _direction; PdfTemplate _template; public CustomFooter(IPdfFont pdfRptFont, PdfRunDirection direction) { _direction = direction; _pdfRptFont = pdfRptFont; _font = _pdfRptFont.Fonts[0]; } public void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData) { _template.BeginText(); _template.SetFontAndSize(_pdfRptFont.Fonts[0].BaseFont, 8); _template.SetTextMatrix(0, 0); _template.ShowText((writer.PageNumber - 1).ToString()); _template.EndText(); } public void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData) { var pageSize = document.PageSize; var text = "Page " + writer.PageNumber + " / "; var textLen = _font.BaseFont.GetWidthPoint(text, _font.Size); var center = (pageSize.Left + pageSize.Right) / 2; var align = _direction == PdfRunDirection.RightToLeft ? Element.ALIGN_RIGHT : Element.ALIGN_LEFT; ColumnText.ShowTextAligned( canvas: _pdfContentByte, alignment: align, phrase: new Phrase(text, _font), x: center, y: pageSize.GetBottom(25), rotation: 0, runDirection: (int)_direction, arabicOptions: 0); var x = _direction == PdfRunDirection.RightToLeft ? center - textLen : center + textLen; _pdfContentByte.AddTemplate(_template, x, pageSize.GetBottom(25)); } public void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData) { _pdfContentByte = writer.DirectContent; _template = _pdfContentByte.CreateTemplate(50, 50); } } }
البته لازم به ذکر است که تمام این کدها به پوشه Samples سورس پروژه نیز جهت سهولت دسترسی، اضافه شدهاند .
توضیحات:
برای پیاده سازی Header و Footer سفارشی در PdfReport نیاز خواهید داشت تا دو اینترفیس IPageHeader و IPageFooter را پیاده سازی کنید.
ساختار IPageHeader را در ذیل ملاحظه میکنید:
using System.Collections.Generic; using iTextSharp.text; using iTextSharp.text.pdf; namespace PdfRpt.Core.Contracts { public interface IPageHeader { PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData); PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData); } }
RenderingGroupHeader مرتبط است به مباحث گروه بندی اطلاعات و گزارشات master-detail که در قسمتهای بعد به آنها اشاره خواهد شد. چون در اینجا به آن نیازی نداشتیم، تنها کافی است متد متناظر با آن، null بر گرداند که در کلاس CustomHeader فوق قابل مشاهده است.
متد RenderingReportHeader به ازای تولید هر صفحه جدید، فراخوانی خواهد شد. به عبارتی میتوانید در صفحات مختلف، هدرهای مختلفی را نمایش دهید.
خروجی هر دو متد در اینجا یک جدول از نوع PdfPTable است. بنابراین هر نوع ساختار دلخواهی را که علاقمند هستید به شکل یک PdfPTable ایجاد کرده و بازگشت دهید. این جدول در هدر صفحات ظاهر خواهد شد.
برای نمونه در کلاس CustomHeader، یک قالب تهیه شده توسط Open Office توسط متد توکار PdfImageHelper.GetITextSharpImageFromPdfTemplate دریافت و تبدیل به تصویر میشود. این تصویر از نوع تصاویر قابل درک توسط iTextSharp است و نه اینکه واقعا تبدیل به یک تصویر معمولی مثلا از نوع bmp شود. سپس این تصویر، در یک ردیف از جدولی قرار داده شده و این جدول بازگشت داده میشود.
در کل یا توسط کار با PdfPTable میتوانید یک هدر غیرپیش فرض را طراحی کنید و یا میتوانید توسط ابزارهای بصری مانند Open Office یک قالب خاص را برای آن تهیه کرده و به روشی که ذکر شد و کدهای آنرا ملاحظه میکنید، بارگذاری و استفاده کنید. این قالبها در مسیر Bin\Data سورسهای پروژه قرار داده شدهاند.
ساختار IPageFooter به صورت زیر است:
using iTextSharp.text; using iTextSharp.text.pdf; using System.Collections.Generic; namespace PdfRpt.Core.Contracts { public interface IPageFooter { void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData); void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData); void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData); } }
برای طراحی یک Footer سفارشی کافی است اینترفیس فوق را پیاده سازی کنید که نمونهای از آنرا در کدهای کلاس CustomFooter ملاحظه مینمائید.
متد DocumentOpened، با وهله سازی شیء Document فراخوانی میشود.
متد PageFinished هر بار پیش از اتمام کار صفحه جاری و افزوده شدن آن به Document فراخوانی میگردد.
متد ClosingDocument، در زمان بسته شدن شیء Document فراخوانی خواهد شد.
اگر به امضای این متدها دقت کنید، شیء PdfWriter در اختیار شما قرار گرفته است که توسط آن میتوان مستقیما بر روی فایل PDF، محتوایی را قرار داد. شیء Document نیز در دسترس است. مثلا توسط آن میتوان اندازه دقیق صفحه را بدست آورد.
به علاوه پارامتر columnCellsSummaryData نیز امکان دسترسی به مقادیر ردیفهای قبلی را در اختیار شما قرار میدهد. برای مثال اگر نیاز دارید تا بر اساس مقادیر ستونها و ردیفهای قبلی، محاسباتی را انجام داده و در پایین صفحات درج کنید، به این ترتیب دسترسی کاملی به آنها، خواهید داشت.
استفاده از این کلاسهای سفارشی نیز همواره به شکل زیر خواهد بود:
readonly CustomHeader _customHeader = new CustomHeader(); //... .PagesFooter(footer => { footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight)); }) .PagesHeader(header => { header.CustomHeader(_customHeader); })
نیازهای یک ورودی تاریخ سازگار با EditForm
- باید قابلیت استفادهی مجدد را داشته باشد. یعنی باید به صورت یک کامپوننت مجزا و یا به صورت یک کتابخانهی مجزا ارائه شود.
- باید با سیستم اعتبارسنجی EditForm یکپارچه باشد.
- باید جنریک باشد. یعنی باید بتوان در صورت نیاز DateTime ، DateTimeOffset و DateOnly و نمونههای nullable آنهارا توسط این کامپوننت دریافت کرد و ورودی و خروجی آن رشتهای نباشد.
نیاز به ارثبری از <InputBase<T جهت ارائهی کامپوننتهایی سازگار با EditForm
تقریبا تمام کامپوننتهای استاندارد EditForm ارائه شدهی توسط Blazor، از کامپوننت پایهای به نام <InputBase<T مشتق میشوند. این کلاس، یک کلاس abstract است که قابلیتهای بیشتری را نسبت به یک input سادهی HTML ای مانند اعتبارسنجی سازگار با EditForm ارائه میدهد. به همین جهت توصیه میشود تا اگر خواستید یک کامپوننت ورودی را برای استفادهی در Blazor و EditForm آن طراحی کنید، با ارثبری از این کلاس شروع کنید و صرفا کار را با یک input ساده، شروع نکنید.
برای استفادهی از آن، ابتدای کامپوننت Blazor ما به این صورت شروع خواهد شد:
@typeparam T @inherits InputBase<T>
protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { // ... } protected override string FormatValueAsString(T? value) { // ... }
ایجاد یک کتابخانهی جدید برای محصور سازی DatePicker جاوااسکریپتی
چون قصد استفادهی مجدد از این کامپوننت جدید را در پروژههای مختلف داریم، بهتر است آنرا تبدیل به یک «کتابخانهی Blazor» کنیم. به همین جهت کتابخانهی فرضی BlazorPersianJavaScriptDatePicker.Lib را در اینجا ایجاد کردهایم.
در ابتدا دو فایل PersianDatePicker.js و PersianDatePicker.css موجود و مدنظر را در پوشههای js و css پوشهی wwwroot این کتابخانه کپی میکنیم. بنابراین استفاده کنندهی از آن، مانند پروژهی blazor wasm جدیدی به نام BlazorPersianJavaScriptDatePicker، باید ارجاعاتی را به آنها به صورت زیر اضافه کند:
<link href="_content/BlazorPersianJavaScriptDatePicker.Lib/css/PersianDatePicker.css" rel="stylesheet"/> <script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/PersianDatePicker.js?v=1"></script>
@using BlazorPersianJavaScriptDatePicker.Lib
شروع به پیاده سازی کامپوننت PersianDatePicker
در ادامه کامپوننت جدید PersianDatePicker.razor را به پروژهی کتابخانه اضافه میکنیم. قسمت razor آن به صورت زیر است:
@typeparam T @inherits InputBase<T> <div> <span style="cursor:pointer" onclick="PersianDatePicker.Show(document.getElementById('@ElementId'), '@Today')"> 📅 </span> <input @attributes="@AdditionalAttributes" type="text" dir="ltr" @ref="ElementReference" name="@ElementId" id="@ElementId" autocapitalize="off" autocorrect="off" autocomplete="off" value="@EnteredValue" @oninput="OnInput"/> @if (ValueExpression is not null) { <ValidationMessage For="@ValueExpression"/> } </div>
در اینجا با کلیک بر روی دکمهی 📅، کار فراخوانی متد PersianDatePicker.Show مربوط به datePicker جاوا اسکریپتی صورت میگیرد. همچنین هر طراحی را که در اینجا ارائه دهیم، قالب UI پیشفرض InputBase را بازنویسی میکند.
نیاز به دریافت تاریخ تنظیم شدهی توسط کدهای جاوااسکریپتی در کامپوننت Blazor
کتابخانههای جاوااسکریپتی با مقداردهی مستقیم textbox.value سبب تغییر مقدار آن میشوند. نکتهی مهم اینجا است که نه فقط Blazor این تغییرات را ردیابی نمیکند، بلکه اگر با استفاده از متد استاندارد جاوااسکریپتی addEventListener به تغییرات این input گوش فرا دهیم، هیچ رخدادی را مشاهده نخواهیم کرد. به همین جهت نیاز است اندکی کدهای PersianDatePicker.js را تغییر دهیم (و این مورد جهت تمام کتابخانههای مشابه یکسان است):
function setValue(date) { _textBox.value = date; // NOTE: To notify the addEventListener('change', fn) _textBox.dispatchEvent(new Event('change')); _textBox.focus(); hide(); try { _textBox.onchange(); }catch(ex) {} }
window.activateDatePicker = { enableDatePicker: function (element, objectReference) { element.addEventListener('change', function (evt) { objectReference.invokeMethodAsync("OnInputFieldChanged", this.value); }); } };
بنابراین این فایل جدید نیز باید به index.html مصرف کننده اضافه شود:
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/activateDatePicker.js?v=1"></script>
فعالسازی DatePicker در اولین بار نمایش کامپوننت Blazor
تا اینجا زیرساخت دریافت مقدار تنظیمی توسط کاربر را در کامپوننت Blazor فراهم کردیم. اکنون نوبت به استفادهی از آن است:
public partial class PersianDatePicker<T> : IDisposable { private bool _isDisposed; private DotNetObjectReference<PersianDatePicker<T>>? _objectReference; private string ElementId { get; } = Guid.NewGuid().ToString("N"); private ElementReference? ElementReference { set; get; } private string Today { get; } = DateTime.Now.ToShortPersianDateString(); [Inject] private IJSRuntime JsRuntime { set; get; } = default!; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _objectReference = DotNetObjectReference.Create(this); await JsRuntime.InvokeVoidAsync("activateDatePicker.enableDatePicker", ElementReference, _objectReference); EnteredValue = CurrentValueAsString; StateHasChanged(); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!_isDisposed) { try { _objectReference?.Dispose(); } finally { _isDisposed = true; } } } }
- همچنین چون نمیخواهیم متد OnInputFieldChanged را به صورت static تعریف کنیم، نیاز است تا یک DotNetObjectReference را ایجاد و به متد enableDatePicker ارسال کرد تا توسط آن بتوان به یک instance method کلاس جاری دسترسی یافت و به سادگی مقادیر کامپوننت را تغییر داد:
[JSInvokable] public void OnInputFieldChanged(string? value)
نیاز به تبدیل T به تاریخ رشتهای و برعکس
زیر ساخت تبدیلات جنریک تاریخ میلادی به شمسی در کتابخانهی « DNTPersianUtils.Core » پیشبینی شدهاست و فقط کافی است از آن استفاده کنیم. با وجود این زیرساخت، تهیهی کامپوننتهای جنریک تاریخ شمسی بسیار ساده میشود:
public partial class PersianDatePicker<T> : IDisposable { private string? _enteredValue; private string? EnteredValue { set => _enteredValue = value; get => UsePersianNumbers ? _enteredValue.ToPersianNumbers() : _enteredValue; } [Parameter] public bool UsePersianNumbers { set; get; } [Parameter] public string ParsingErrorMessage { get; set; } = "لطفا در ورودی {0} تاریخ شمسی معتبری را وارد نمائید."; [Parameter] public int BeginningOfCentury { set; get; } = 1400; private void OnInput(ChangeEventArgs e) { SetCurrentValue(e.Value as string); } private void SetCurrentValue(string? value) { EnteredValue = value; CurrentValueAsString = value; } [JSInvokable] public void OnInputFieldChanged(string? value) { SetCurrentValue(value); } protected override void OnInitialized() { base.OnInitialized(); SanityCheck(); } protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName); if (!value.TryParsePersianDateToDateTimeOrDateTimeOffset(out result, BeginningOfCentury)) { return false; } if (result is null) { throw new InvalidOperationException(validationErrorMessage); } validationErrorMessage = null; return true; } protected override string FormatValueAsString(T? value) { return !string.IsNullOrWhiteSpace(EnteredValue) ? EnteredValue : value.FormatDateToShortPersianDate(); } private void SanityCheck() { if (!Value.IsDateTimeOrDateTimeOffsetType()) { throw new InvalidOperationException( "The `Value` type is not a supported `date` type. DateTime, DateTime?, DateTimeOffset and DateTimeOffset? are supported."); } } // ... }
- InputBase به همراه یک خاصیت عمومی دوطرفهی Value است که امکان تعریفی مانند bind-Value@ را میسر میکند.
- این Value به همراه یک خاصیت متناظر رشتهای به نام CurrentValueAsString نیز هست که در اینجا از آن استفاده میکنیم و کار با آن، بایندینگ دوطرفه و همچنین اعتبارسنجی خودکار و فعالسازی متدهای بازنویسی شدهی InputBase را میسر میکند.
- پیاده سازی متدهای بازنویسی شدهی جنریک TryParseValueFromString و FormatValueAsString، با استفاده از دو متد TryParsePersianDateToDateTimeOrDateTimeOffset و FormatDateToShortPersianDate کتابخانهی « DNTPersianUtils.Core » انجام شدهاند و اصل کار تهیهی یک کامپوننت جنریک تاریخ شمسی را انجام میدهند.
استفادهی از کامپوننت Blazor تهیه شده
یک کامپوننت تاریخ شمسی باید بتواند تمام حالات و نوعهای زیر را پوشش دهد که به لطف جنریک بودن کامپوننت تهیه شده، این امر میسر است:
using System.ComponentModel.DataAnnotations; namespace BlazorPersianJavaScriptDatePicker.ViewModels; public class InputPersianDateViewModel { [Required] public string Name { set; get; } = default!; [Required] public DateTime BirthDayGregorian { set; get; } = DateTime.Now.AddYears(-40); public DateTime? LoginAt { set; get; } = DateTime.Now.AddMinutes(-2); [Required] public DateTimeOffset LogoutAt { set; get; } public DateTimeOffset? RegisterAt { set; get; } = DateTimeOffset.Now.AddMinutes(-10); }
<EditForm Model="Model" OnValidSubmit="DoSave"> <DataAnnotationsValidator/> <div> <label>تاریخ تولد</label> <div> <PersianDatePicker @bind-Value="Model.BirthDayGregorian" UsePersianNumbers="false" /> </div> </div> <button type="submit">ارسال</button> </EditForm>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorPersianJavaScriptDatePicker.zip
OpenCVSharp #7
اینترفیس یا API زبان C کتابخانهی OpenCV مربوط است به نگارشهای 1x این کتابخانه و تمام مثالهایی را که تاکنون ملاحظه کردید، بر مبنای همین اینترفیس تهیه شده بودند. اما از OpenCV سری 2x، این اینترفیس صرفا جهت سازگاری با نگارشهای قبلی، نگهداری میشود و اینترفیس اصلی مورد استفاده، API جدید ++C آن است. به همین جهت کتابخانهی OpenCVSharp نیز در فضای نام OpenCvSharp.CPlusPlus و توسط اسمبلی OpenCvSharp.CPlusPlus.dll، امکان دسترسی به این API جدید را فراهم کردهاست که در ادامه نکات مهم آنرا بررسی خواهیم کرد.
تبدیل مثالهای اینترفیس C به اینترفیس ++C
مثال «تبدیل تصویر به حالت سیاه و سفید» قسمت سوم را درنظر بگیرید. این مثال به کمک اینترفیس C کتابخانهی OpenCV کار میکند. معادل تبدیل شدهی آن به اینترفیس ++C به صورت ذیل است:
// Cv2.ImRead using (var src = new Mat(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor)) using (var dst = new Mat()) { Cv2.CvtColor(src, dst, ColorConversion.BgrToGray); // How to export using (var bitmap = dst.ToBitmap()) // => OpenCvSharp.Extensions.BitmapConverter.ToBitmap(dst) { bitmap.Save("gray.png", ImageFormat.Png); } using (new Window("BgrToGray C++: src", image: src)) using (new Window("BgrToGray C++: dst", image: dst)) { Cv2.WaitKey(); } }
- بجای IplImage، از کلاس Mat استفاده شدهاست.
- برای ایجاد Clone یک تصویر نیازی نیست تا پارامترهای خاصی را به Mat دوم (همان dst) انتساب داد و ایجاد یک Mat خالی کفایت میکند.
- اینبار بجای کلاس Cv اینترفیس C، از کلاس Cv2 اینترفیس ++C استفاده شدهاست.
- متد الحاقی ToBitmap نیز که در کلاس OpenCvSharp.Extensions.BitmapConverter قرار دارد، با نمونهی Mat سازگار است و به این ترتیب میتوان خروجی معادل دات نتی Mat را با فرمت Bitmap تهیه کرد.
- بجای CvWindow، در اینجا باید از Window سازگار با Mat، استفاده شود.
- new Mat معادل Cv2.ImRead است. بنابراین اگر مثال ++C ایی را در اینترنت یافتید:
cv::Mat src = cv::imread ("foo.jpg"); cv::Mat dst; cv::cvtColor (src, dst, CV_BGR2GRAY);
کار مستقیم با نقاط در OpenCVSharp
متدهای ماتریسی OpenCV، فوق العاده در جهت سریع اجرا شدن و استفادهی از امکانات سخت افزاری و پردازشهای موازی، بهینه سازی شدهاند. اما اگر قصد داشتید این متدهای سریع را با نمونههایی متداول و نه چندان سریع جایگزین کنید، میتوان مستقیما با نقاط تصویر نیز کار کرد. در ادامه قصد داریم کار فیلتر توکار Not را که عملیات معکوس سازی رنگ نقاط را انجام میدهد، شبیه سازی کنیم.
در اینجا نحوهی دسترسی مستقیم به نقاط تصویر بارگذاری شده را توسط اینترفیس C، ملاحظه میکنید:
using (var src = new IplImage(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor)) using (var dst = new IplImage(src.Size, src.Depth, src.NChannels)) { for (var y = 0; y < src.Height; y++) { for (var x = 0; x < src.Width; x++) { CvColor pixel = src[y, x]; dst[y, x] = new CvColor { B = (byte)(255 - pixel.B), G = (byte)(255 - pixel.G), R = (byte)(255 - pixel.R) }; } } // [C] Accessing Pixel // https://github.com/shimat/opencvsharp/wiki/%5BC%5D-Accessing-Pixel using (new CvWindow("C Interface: Src", image: src)) using (new CvWindow("C Interface: Dst", image: dst)) { Cv.WaitKey(0); } }
روش ارائه شدهی در اینجا یکی از روشهای دسترسی به نقاط، توسط اینترفیس C است. سایر روشهای ممکن را در Wiki آن میتوانید مطالعه کنید.
شبیه به همین کار را میتوان به نحو ذیل توسط اینترفیس ++C کتابخانهی OpenCVSharp نیز انجام داد:
// Cv2.ImRead using (var src = new Mat(@"..\..\Images\Penguin.Png", LoadMode.AnyDepth | LoadMode.AnyColor)) using (var dst = new Mat()) { src.CopyTo(dst); for (var y = 0; y < src.Height; y++) { for (var x = 0; x < src.Width; x++) { var pixel = src.Get<Vec3b>(y, x); var newPixel = new Vec3b { Item0 = (byte)(255 - pixel.Item0), // B Item1 = (byte)(255 - pixel.Item1), // G Item2 = (byte)(255 - pixel.Item2) // R }; dst.Set(y, x, newPixel); } } // [Cpp] Accessing Pixel // https://github.com/shimat/opencvsharp/wiki/%5BCpp%5D-Accessing-Pixel //Cv2.NamedWindow(); //Cv2.ImShow(); using (new Window("C++ Interface: Src", image: src)) using (new Window("C++ Interface: Dst", image: dst)) { Cv2.WaitKey(0); } }
میتوانید سایر روشهای دسترسی به نقاط را توسط اینترفیس ++C، در Wiki این کتابخانه مطالعه نمائید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.