مطالب
HTML5 Web Component - قسمت دوم - Custom Elements
Custom Elements، دارای یک چرخه حیات می‌باشند. در طی این چرخه حیات، می‌توان تعدادی متد خاص را به المان سفارشی خود اضافه کرد که به صورت خودکار توسط مرورگر فراخوانی می‌شوند. به این متدها Life-cycle Callbacks یا Custom Element Reactions نیز می‌گویند. برای درک بهتر چرخه حیات مذکور، به تکه کد زیر توجه نمائید:
customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
        console.log('constructed!');
    }

    connectedCallback() {
        console.log('connected!');
    }

    disconnectedCallback() {
        console.log('disconnected!');
    }

    adoptedCallback() {
        console.log('adopted!');
    }

    attributeChangedCallback(name, oldValue, newValue) {
        console.log('attirbuteChanged!', name, oldValue, newValue);
    }

    static get observedAttributes() {
        return ['checked','demo','label'];
    }
});

Element Upgrades
به صورت پیش‌فرض، المان‌های موجود در DOM که مبتنی‌بر استانداردهای HTML تعریف نشده‌ا‌ند، توسط مرورگر به عنوان HTMLUnknownElement تجزیه و تحلیل خواهند شد. ولی این موضوع برای المان‌های سفارشی که نام معتبری دارند (وجود «-»)، صدق نمی‌کند. برای مثال دو خط کد زیر را در کنسول مربوط به Developer tools مرورگر خود اجرا کنید:
// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true //true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true //true
در این صورت، امکان استفاده از المان سفارشی، قبل از معرفی و ثبت آن توسط متد customElements.define نیز وجود خواهد داشت. یعنی اگر در DOM شما تعدادی المان سفارشی وجود داشته باشند که به هر دلیل نیاز است پس از گذشت یک بازه زمانی کوتاهی معرفی و ثبت شوند (مثال: lazy load اسکریپت‌های متناظر با المان‌های سفارشی در Angular)، این المان‌ها معتبر هستند. فرآیند فراخوانی متد define و استفاده از کلاس معرفی شده برای ارتقاء المان سفارشی موجود در DOM، اصطلاحا Element Upgrades نامیده می‌شود. همچنین با استفاده از متد customElements.whenDefined که یک Promise را بازگشت می‌دهد، می‌توان از معرفی و ثبت شدن المان خاصی آگاه شد:
customElements.whenDefined('x-component').then(() => {
  console.log('x-component defined');
});
یا حتی امکان استفاده از سلکتور «‎:defined‏» نیز به شکل زیر وجود دارد:
<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>

// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map(socialButton => {
  return customElements.whenDefined(socialButton.localName);
));

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});
در اینجا ابتدا تمام المان‌های تعریف نشده، کوئری شده‌اند و با استفاده از متد map و اجرای متد whenDefined، به لیستی از Promiseها رسیده‌ایم و در نهایت با استفاده از Promise.all منتظر اتمام مرحله upgrade المان‌های مذکور هستیم.
سازنده مرتبط با کلاس المان سفارشی x-component، در هنگام وهله‌سازی یا فرآیند upgrades فراخوانی می‌شود و می‌تواند برای مقداردهی اولیه خواص وهله جاری، تنظیم رخدادگردان‌ها (Event Listeners) و یا ایجاد و اتصال ShadowDOM با استفاده از متد attachShadow، محل مناسبی باشد. طبق مستندات مرتبط، فراخوانی ()super بدون ارسال هیچ آرگومانی باید در اولین خط این سازنده انجام شود.
برای وهله‌سازی المان سفارشی نیز می‌توان از متد customeElements.get به شکل زیر استفاده کرد:
customeElements.define('x-component',...)
let XComponent = customElements.get('x-component');
document.body.appendChild(new XComponent())
متد get، ارجاعی به سازنده کلاس x-component را بازگشت خواهد داد.

connectedCallback 
 اولین متد بعد از فراخوانی سازنده، connectedCallback نام دارد و زمانی رخ می‌دهد که وهله‌ای از یک المان سفارشی به Light DOM افزوده شده‌است و یا توسط Parser مرورگر، در DOM شناسایی شود. این متد، محل پیشنهاد شده برای اجرای کدهای زمان راه‌اندازی مانند دریافت منابع از سرور و یا رندر کردن محتوایی خاص، می‌باشد. 
نکته: این متد ممکن است بیش از یکبار نیز فراخوانی شود! هنگامیکه یک المان موجود در DOM از طریق کد از DOM جداشده و سپس اضافه شود:
const el = document.createElement('x-component');
document.body.appendChild(el);
// connectedCallback() called

el.remove();
// disconnectedCallback()

document.body.appendChild(el);
// connectedCallback() called again


disconnectedCallback
این متد نیز هر وقت المانی از DOM حذف شود، اجرا خواهد شد و مانند متد connectedCallback ممکن است چندین بار فراخوانی شود. همچنین محل مناسبی برای آزادسازی منابع استفاده شده در پیاده‌سازی المان سفارشی، می‌باشد. مانند: استفاده از متد clearInterval برای پاکسازی یک تایمر که با متد setInterval ایجاد شده‌است.  
نکته: هیچ تعهدی به اجرای متد disconnectedCallback در تمام حالاتی که یک المان از DOM حذف می‌شود، وجود ندارد. به عنوان مثال هنگامیکه یک برگه‌ی مرورگر بسته شود، این متد فراخوانی نخواهد شد.

‌attributeChangedCallback
 این متد هنگامی فراخوانی خواهد شد که خصوصیات مشخص شده از طریق observedAttributes به عنوان یک static getter، اضافه، حذف، ویرایش و یا جایگزین شوند. همچنین در مرحله Upgrades برای مقادیر اولیه خصوصیات که توسط استفاده کننده از المان سفارشی، مشخص شده‌است نیز فراخوانی می‌شود.
adoptedCallback
متدهای قبلی بیشترین استفاده را دارند؛ این متد خاص نیز زمانیکه یک المان سفارشی به یک DOM دیگری منتقل می‌شود، اجرا خواهد شد. برای مثال:
const iframe = document.querySelector('iframe');
const iframeImages = iframe.contentDocument.querySelectorAll('img');
const newParent = document.getElementById('images');

iframeImages.forEach(function(imgEl) {
  newParent.appendChild(document.adoptNode(imgEl));
});
در تکه کد بالا، لیست المان‌های img موجود در داخل iframe کوئری شده و سپس با پیمایش بر روی لیست بدست آمده و در زمان فراخوانی متد adoptNode که کار تغییر ownerDocument مرتبط با یک المان را انجام می‌دهد، متد adoptedCallback ما نیز اجرا خواهد شد.

Methods

اگر المان‌های بومی و استاندارد موجود را بررسی کنید، همه آنها دارای یکسری متد، پراپرتی و صفات مشخصی هستند. در اینجا نیز تعریف رفتاری برای یک المان سفارشی و کپسوله، نکته خاصی ندارد و به شکل زیر قابل تعریف و استفاده می‌باشد:
class XComponent extends HTMLElement {
  constructor() {
    super();
  }
  
  doSomething(){
    console.log('doSomething');
  }
}

let component = document.querySelector('x-component');
component.doSomething();
مشخص است که امکان تعریف انواع و اقسام متدها با پارامترها و خروجی‌های مختلفی و حتی نسخه‌های همزمان یا ناهمزمان آن‌ها وجود دارد.

Attributes & Properties

در HTML خیلی رایج است که مقادیر پراپرتی‌های یک المان در قالب یکسری صفات، نمودی در DOM هم داشته باشند. برای مثال:
div.id = 'id-value';
div.hidden = true;
انتظار چنین خروجی داریم:
<div id="id-value" hidden>
این موضوع، تحت عنوان «Reflecting Properties to Attributes» مطرح می‌باشد. در این صورت علاوه بر اینکه با یک نگاه به DOM، از مقادیر خصوصیات یک المان آگاه خواهیم بود، امکان استفاده از این صفات به عنوان سلکتورهایی در زمان استایل‌دهی نیز وجود دارد. حال از این مکانیزم برای اعمال یکسری صفات دسترسی‌پذیری مانند صفات ARIA به المان سفارشی خود نیز می‌توان استفاده کرد. برای مثال:
class XComponent extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this._render();
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }

    _render() {
        //... 
    }
}
ایده کار خیلی ساده است؛ پراپرتی‌های یک المان‌سفارشی را از طریق متدهای getter و setter تعریف کرده و در بدنه پیاده‌سازی آنها، صفات HTML ای المان جاری را تغییر داده و یا از مقادیر آنها استفاده کنیم.
اینبار با مقداردهی پراپرتی disabled برروی وهله‌ای از المان سفارشی ما، این مقادیر نمودی در DOM هم خواهند داشت. با استفاده از متدهای setAttribute یا removeAttribute کار همگام‌سازی پراپرتی‌ها با صفات را انجام داده‌ایم.
همچین با استفاده از متد attributeChangedCallback نیز می‌توان برای اعمال صفات ARIA که اشاره شد، به شکل زیر استفاده کرد:
attributeChangedCallback(name, oldValue, newValue) {
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-disabled', !!newValue);
      break;
    ...
  }
یا حتی به شکل زیر:
attributeChangedCallback(name, oldValue, newValue) {
    // When the component is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the other attribute changing.
  }
در اینجا از همان getter که طبق پیاده‌سازی ما، در پشت صحنه از همان مقدار صفت disabled استفاده می‌کند، برای تنظیم یکسری صفات دیگر استفاده کرده‌ایم. به عنوان مثال اگر المان ما غیرفعال شده بود، صفت tabindex آن را با «‎-1‏» مقداردهی می‌کنیم تا از توالی پیمایش مبتنی‌بر Tab خارج شود.
نکته: پیشنهاد می‌شود از مکانیزم همگام‌سازی پراپرتی‌ها و صفات، برای انواع داده‌ای اولیه (رشته، عدد و ...) استفاده شود و برای دریافت مقادیری مانند objects و یا arrays، از متدها یا پراپرتی‌ها استفاده کنید. در غیر این صورت نیاز خواهد بود که این مقادیر را سریالایز و در داخل المان سفارشی، عملیات معکوس آن را انجام دهید که می‌تواند هزینه‌ی زیادی داشته باشد. عملیات سریالایز نیز خود باعث از دست دادن ارجاعات به آن مقادیر خواهد شد. به صورت کلی هیچکدام از المان‌های بومی موجود، چنین اطلاعاتی را دریافت نمی‌کند. برای مثال:
constructor() {
    super();

    this._data = [];
}

get data() {
    return _data;
}

set data(value) {
    if (this_data === value) return;
    this._data = value;
    this._render();
}

Lazy Properties

همانطور که اشاره شد حتی قبل از مرحله upgrades مربوط به المان‌های سفارشی استفاده شده در سند HTML برنامه شما، به عنوان المان‌های معتبری هستند که امکان کوئری کردن و مقداردهی اولیه خصوصیات آنها از طریق کد نیز ممکن است. این موضوع زمانیکه از فریم‌ورکی مثل Angular استفاده می‌شود، المان موردنظر به صورت خودکار توسط فریم‌ورک لود و به صفحه اضافه شده و در انتهای عملیات، binding پراپرتی‌های آن به خصوصیات موجود در کامپوننت Angular ای انجام خواهد شد. به مثال زیر توجه کنید:
<x-component [disabled]="model.disabled"></x-component>
در این صورت اگر لود اسکریپت، معرفی و ثبت این المان سفارشی به صورت Lazy انجام شود، امکان آن وجود دارد که فریم‌ورک، عملیات binding را قبل از مرحله upgrades انجام دهد. خوب... این موضوع چه مشکلی را ایجاد می‌کند؟ در این صورت چون مرحله upgrades تمام نشده است، پیاده‌سازی بدنه متد setter متناظر با خصوصیات المان سفارشی، توسط پراپرتی جدیدی که توسط فریم‌ورک برروی وهله موجود تعریف می‌شود، بی‌استفاده خواهد ماند. برای مثال، اگر سعی کنیم قبل از مرحله upgrades خصوصیت disabled المان x-component را مقداردهی کنیم، عملیات مکانیزم همگام‌سازی مدنظر ما اجرا نخواهد شد:
let el = document.querySelector('x-component');
el.disabled = true;

customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }
});

با این خروجی مواجه خواهیم شد که هیچ اثری از صفت disabled دیده نمی‌شود:


یکی از روش‌های پیشنهاد شده برای حل این مشکل، مقداردهی مجدد پراپرتی‌ها بعد از مرحله upgrades و پس از اینکه متد setter تعریف شده‌است، می‌باشد:
let el = document.querySelector('x-component');
el.disabled = true;

customElements.define("x-component", class extends HTMLElement {
    constructor() {
        super();
    }

    connectedCallback() {
        this._upgradeProp('disabled');
    }

    get disabled() {
        return this.hasAttribute('disabled');
    }

    set disabled(val) {
        // Reflect the value of `disabled` as an attribute.
        if (val) {
            this.setAttribute('disabled', '');
        } else {
            this.removeAttribute('disabled');
        }

        this._render();
    }

    _upgradeProp(prop) {
        if (this.hasOwnProperty(prop)) {
            let value = this[prop];
            delete this[prop]; //delete instance property
            this[prop] = value; // set prototype property
        }
    }
});
ایده کار به این صورت است که مقدار پراپرتی مورد نظر را که قبل از مرحله upgrades برروی وهله جاری (instance property) تنظیم شده‌است، در متغییری نگهداری کرده و آن پراپرتی را حذف و سپس پراپرتی تعریف شده در کلاس (prototype property) را برای وهله جاری مقداردهی کنیم.
نکته: به یاد داشته باشید که قبل از اینکه یکسری صفات خاص را مقدار دهی کنید، بررسی شود که استفاده کننده از المان سفارشی، مقداری را تنظیم نکرده باشد. برای مثال اگر قصد دارید المان سفارشی شما قابلیت focus را داشته باشد، نیاز است شما حداقل tabindex=-1 را تنظیم کنید؛ حتی اگر استفاده کننده، آن را مقداردهی نکرده باشد:
connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', -1); //element is not reachable via sequential keyboard navigation, but could be focused
}

مطالب
تفاوت AngularJS با KnockoutJS
با پیشرفت HTML 5 و پدید آمدن چارچوب‌های مختلف JavaScript توسعه‌ی نرم افزار‌های تک صفحه ای تحت وب (Single Page Applications) محبوب شده است. 
اخیرا مطالب خوبی در رابطه با AngularJS در وبسایت جاری منتشر شده است. KnockoutJS توسط Microsoft معرفی شد و در قالب پیشفرض پروژه‌های SPA قرار گرفت ، بنابراین احتمالا این سوال برای افرادی مطرح شده است که تفاوت بین KnockoutJS و AngularJS چیست ؟ 
می توان پاسخ داد این مقایسه ممکن نیست. 
KnockoutJS : یک پیاده سازی مستقل JavaScript از الگوی MVVM با امکانات Databinding می‌باشد. Knokcout یک کتابخانه‌ی Databinding است نه یک کتاب خانه‌ی SPA
AngularJS : طبق معرفی در این مطلب AngularJS فریم ورکی متن باز و نوشته شده به زبان جاوا اسکریپت است. هدف از به وجود آمدن این فریم ورک، توسعه هر چه ساده‌تر SPA‌ها با الگوی طراحی MVC و تست پذیری هر چه آسان‌تر آن‌ها است. این فریم ورک توسط یکی از محققان Google در سال 2009 به وجود آمد. بعد‌ها این فریم ورک تحت مجوز MIT به صورت متن باز در آمد و اکنون گوگل آن را حمایت می‌کند و توسط هزاران توسعه دهنده در سرتاسر دنیا، توسعه داده می‌شود. 

بنابراین شاید بهتر باشد ذکر شود AngularJS یک Presentation Framework مخصوص برنامه‌های وب تک صفحه ای می‌باشد در حالی که KnockoutJS کتاب خانه ای با تمرکز بر Databinding می‌باشد ، بنابراین مقایسه‌ی این‌ها چندان صحیح نیست.

اگر قصد بر بررسی گزینه‌های دیگر در کنار Angular باشد ، می‌توان از Durandal نام برد. Durandal یک چارچوب SPA می‌باشد ، این چارچوب بر فراز jQuery ، RequireJS و Knockout توسعه پیدا کرده است. (سابقا برای routing از SammyJS استفاده می‌کرد که در نسخه‌های اخیر از موتور خودش استفاده می‌کند.)

Durandal از Knockout جهت Databinding و از RequireJS برای مدیریت وابستگی‌ها استفاده می‌کند.
Angular همه‌ی امکانات بالا را مستقل پیاده سازی کرده و حتی نیازی به jQuery ندارد. اگر jQuery وجود داشته باشد Angular از آن استفاده می‌کند در غیر این صورت از jQuery Lite یا jqLite استفاده می‌کند. jqLite پیاده سازی توابع متداول jQuery برای دستکاری DOM می‌باشد. اطلاعات بیشتر در اینجا

بنابراین با استفاده تنها از KnockoutJS نمی‌توان یک برنامه‌ی کامل SPA توسعه داد ، در کنار آن نیاز به کتابخانه‌های دیگری مثل jQuery برای مدیریت درخواست‌های  AJAX و استفاده از دیگر API‌ها ، Sammy برای routing و RequireJS برای مدیریت وابستگی‌ها می‌باشد.

در Knockout و در نتیجه Durandal عمل Databinding به این صورت است :
// JavaScript
var vm = {
    firstName = ko.observable('John')
};
ko.applyBindings(vm);
<!-- HTML -->
<input data-bind="value:firstName"/>
در Angular :
// JavaScript
// Inside of a personController
this.firstName = 'John';
در Angular همچنین از یک روش Controller As استفاده می‌شود :
<!-- HTML -->
<div ng-controller="personController as vm">
    <input ng-model="vm.firstName"/>
</div>
اگر تنها نیاز به یک کتابخانه‌ی Databinding باشد ، Knockout گزینه‌ی مناسبی است ، به خوبی از عمل مقید سازی داده‌ها پشتیبانی می‌کند و Syntax خوش دستی دارد اما اگر نیاز به چارچوبی برای توسعه‌ی پروژه‌های SPA می‌باشد می‌توان از Angular یا Durandal استفاده کرد. 
مقایسه‌ی Knockout با Angular همانند مقایسه‌ی موتور بنز با ماشین پورشه می‌باشد. 



مطالب
تهیه پردازنده‌های سفارشی برای افزونه XMLWorker کتابخانه iTextSharp
پیشتر مطلب «تهیه پردازنده‌های سفارشی برای HTMLWorker کتابخانه iTextSharp» را در این سایت مطالعه کرده‌اید. از آنجائیکه افزونه HTMLWorker منسوخ شده است و دیگر پشتیبانی نخواهد شد، باید کدهای فعلی را به افزونه XMLWorker منتقل کرد. مقدمه‌ای را در این زمینه در مطلب «تبدیل HTML فارسی به PDF با استفاده از افزونه‌ی XMLWorker کتابخانه‌ی iTextSharp» می‌توانید مطالعه نمائید.
در ادامه قصد داریم همان امکان پشتیبانی از تصاویر base64 مدفون شده در صفحات HTML را به کتابخانه XMLWorker نیز اضافه کنیم.


تهیه پردازنده‌های سفارشی تگ‌های HTML جهت افزونه XMLWorker

در اینجا کار با ارث بری از کلاس AbstractTagProcessor شروع می‌شود. سپس کافی است تا متد End این کلاس پایه را override کرده و تگ سفارشی خود را پردازش کنیم. نمونه‌هایی از این نوع پردازنده‌ها را در پوشه itextsharp.xmlworker\iTextSharp\tool\xml\html سورس‌های iTextSharp می‌توانید ملاحظه کنید و جهت ایده گرفتن بسیار مناسب می‌باشند.
یکی از این پردازنده‌های پیش فرض موجود در افزونه XMLWorker کار پردازش تگ‌های img را انجام می‌دهد و در کلاس iTextSharp.tool.xml.html.Image قرار گرفته است. این پردازنده به صورت پیش فرض تصاویر base64 را پردازش نمی‌کند. برای رفع این مشکل می‌توان به نحو ذیل عمل کرد:
    public class CustomImageTagProcessor : iTextSharp.tool.xml.html.Image
    {
        public override IList<IElement> End(IWorkerContext ctx, Tag tag, IList<IElement> currentContent)
        {
            IDictionary<string, string> attributes = tag.Attributes;
            string src;
            if (!attributes.TryGetValue(HTML.Attribute.SRC, out src))
                return new List<IElement>(1);

            if (string.IsNullOrEmpty(src))
                return new List<IElement>(1);

            if (src.StartsWith("data:image/", StringComparison.InvariantCultureIgnoreCase))
            {
                // data:[<MIME-type>][;charset=<encoding>][;base64],<data>
                var base64Data = src.Substring(src.IndexOf(",") + 1);
                var imagedata = Convert.FromBase64String(base64Data);
                var image = iTextSharp.text.Image.GetInstance(imagedata);

                var list = new List<IElement>();
                var htmlPipelineContext = GetHtmlPipelineContext(ctx);
                list.Add(GetCssAppliers().Apply(new Chunk((iTextSharp.text.Image)GetCssAppliers().Apply(image, tag, htmlPipelineContext), 0, 0, true), tag, htmlPipelineContext));
                return list;
            }
            else
            {
                return base.End(ctx, tag, currentContent);
            }
        }
    }
با ارث بری از کلاس پردازنده پیش فرض تگ‌های تصاویر یا iTextSharp.tool.xml.html.Image شروع و سپس متد End آن‌را  تحریف کرده‌ایم.
در اینجا اگر src یک تگ img با الگوی تصاویر base64 شروع شده باشد، تصویر آن استخراج و تبدیل به وهله‌ای از تصاویر استاندارد iTextSharp می‌شود. سپس این تصویر در اختیار htmlPipelineContext قرار داده خواهد شد و یا اگر این تصویر از نوع base64 نباشد، همان متد base.End کلاس پایه، جهت پردازش آن کفایت می‌کند.


استفاده از یک پردازنده تگ سفارشی HTML افزونه XMLWorker

تا اینجا یک پردازنده جدید تصاویر را ایجاد کرده‌ایم. برای اینکه XMLWorker را از وجود آن مطلع کنیم، نیاز است آن‌را به درون htmlPipeline این افزونه تزریق نمائیم که کدهای کامل آن‌را در ذیل مشاهده می‌کنید:
            using (var doc = new Document(PageSize.A4))
            {
                var writer = PdfWriter.GetInstance(doc, new FileStream("test.pdf", FileMode.Create));
                doc.Open();
                var html = @"<img src='data:image/png;base64,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' width='62' height='80' style='float: left; margin-right: 28px;' />";

                var tagProcessors = (DefaultTagProcessorFactory)Tags.GetHtmlTagProcessorFactory();
                tagProcessors.RemoveProcessor(HTML.Tag.IMG); // remove the default processor
                tagProcessors.AddProcessor(HTML.Tag.IMG, new CustomImageTagProcessor()); // use our new processor

                var cssResolver = new StyleAttrCSSResolver();
                cssResolver.AddCss(@"code { padding: 2px 4px; }", "utf-8", true);
                var charset = Encoding.UTF8;
                var hpc = new HtmlPipelineContext(new CssAppliersImpl(new XMLWorkerFontProvider()));
                hpc.SetAcceptUnknown(true).AutoBookmark(true).SetTagFactory(tagProcessors); // inject the tagProcessors
                var htmlPipeline = new HtmlPipeline(hpc, new PdfWriterPipeline(doc, writer));
                var pipeline = new CssResolverPipeline(cssResolver, htmlPipeline);
                var worker = new XMLWorker(pipeline, true);
                var xmlParser = new XMLParser(true, worker, charset);
                xmlParser.Parse(new StringReader(html));
            }
            Process.Start("test.pdf");
در اینجا ابتدا لیست پردازنده‌های پیش فرض افزونه XMLWorker را دریافت و سپس پردازنده تگ img آن‌را حذف و با نمونه جدید خود جایگزین کرده‌ایم. در ادامه این لیست تغییر یافته به درون HtmlPipelineContext تزریق شده‌است تا بجای DefaultTagProcessorFactory اصلی مورد استفاده قرار گیرد.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 13 - معرفی View Components
نکته تکمیلی :
ایجاد منوهای چند سطحی با استفاده از ViewComponent تا N سطح
کلاس Entity :
public class Navbar
    {
        public int Id { get; set; }
        public string Title { get; set; }

        public int? ParentId { get; set; }
        public virtual Navbar Parent { get; set; }
        public bool IsActive { get; set; }
        public bool HasChiled { get; set; }
        public bool IsMegaMenu { get; set; }
        public PageGroup PageGroup { get; set; }
        public string Url { get; set; }
        public bool OpenNewPage { get; set; }

        public virtual ICollection<Navbar> Children { get; set; }
    }
کلاس ViewComponent :
    public class TopNavbar : ViewComponent
    {
        private readonly DbSet<Navbar> _navbars;
        private readonly AppDbContext _dbContext;

        public TopNavbar(AppDbContext dbContext)
        {
            _dbContext = dbContext;
            _navbars = _dbContext.Set<Navbar>();
        }
        public async Task<IViewComponentResult> InvokeAsync()
        {
            var navbars = await _navbars.Include(p=>p.Parent).Include(x=>x.Children).OrderBy(x=>x.ParentId).ToListAsync();
            return View(viewName: "~/Views/Shared/Components/NavbarViewComponent/_Menu.cshtml", navbars);
        }
    }
فراخوانی viewcomponent در Layout.cshtml
 <ul class="menu">
                <li>
                    <a href="Index_demo6.html"><i class="menu_icon_wrapper fal fa-home-lg-alt"></i>صفحه اصلی</a>
                </li>
                @await Component.InvokeAsync("TopNavbar");                
 </ul>
Menu.cshtml_ :
@using TR.Context.Entities
@using Microsoft.AspNetCore.Html
@model IEnumerable<TR.Context.Entities.Navbar>


@foreach (var menu in Model.Where(x => x.Parent == null))
{

    <li class="@(menu.HasChiled ? "has_sub narrow" : "")">
        <a href="#">@menu.Title</a>
        @if (menu.HasChiled)
        {
            <div class="second">
                <div class="inner">
                    <ul>
                        @foreach (var menuChild in menu.Children)
                        {
                        <partial name="~/Views/Shared/Components/NavbarViewComponent/_SubMenu.cshtml" model="menuChild" />
                        }
                    </ul>
                </div>
            </div>
        }
    </li>
}
پارشیال ویو SubMenu.cshtml_
@model TR_.Context.Entities.Navbar

<li class="@(Model.HasChiled ? "sub":"")">
    <a href="#">
        @if (Model.Children.Any())
        {<i class="q_menu_arrow fal fa-angle-left"></i>}
        @Model.Title
    </a>
    @if (Model.Children.Any())
    {
        <ul>
            @foreach (var menuChild in Model.Children)
            {
                <partial name="~/Views/Shared/Components/NavbarViewComponent/_SubMenu.cshtml" model="menuChild" />
            }
        </ul>
    }
</li>
با این خروجی :

مطالب
مسیریابی در AngularJs #بخش اول
در مطالب قبل کنترلر‌ها و view‌ها مورد بحث قرار گرفتند. در این پست در نظر داریم یکی از ویژگی‌های دیگر AngularJs به نام مسیر یابی (Routing) را مورد بحث قرار دهیم.
یکی از ویژگی‌های برنامه‌های تک صفحه ای عدم Reload شدن صفحات است ،بر خلاف برنامه‌های وب چند صفحه ای که برای نمایش صفحه ای دیگر ، باید از صفحه ای به صفحه ای دیگر منتقل شد و عمل Reload هم به طبع نیز اتفاق می‌افتد.
در قسمت اول  این سری مقالات ، مزایای برنامه‌های وب تک صفحه ای SPA به صورت کاملتری  بیان شده است. در ادامه ما قصد داریم برنامه‌ی وب خود را به صفحات مختلف تقسیم کنیم و سپس با استفاده از امکان مسیریابی موجود در AngularJs آن صفحات را که هر کدام به کنترلری مجزا مقید شده اند، در صفحه‌ی اصلی خود بارگزاری کنیم. همچین استفاده از مسیریابی موجود، میتواند به ما در مدیریت بهتر صفحات کمک فراوانی بکند.
به تصویر زیر دقت کنید :  

در تصویر بالا دو مسیر با آدرس‌های : ShowPage1/  و ShowPage2/  تعریف شده است که هر کدام به یک view مشخص و یک Controller برای مدیریت آن اشاره میکند.

زمانی که ما از تزریق وابستگی‌ها در AngularJs استفاده میکنیم و یک شیء را به کنترلر تزریق میکنیم، Angular توسط Injector$  سعی در پیدا کردن وابستگی مربوطه و سپس تزریق آن به کنترلر را انجام میدهد. برای استفاده از امکان مسیریابی Route ، ما نیز باید از پروایدر مخصوص آن برای تزریق استفاده کنیم. در Angular مسیر‌های برنامه توسط پروایدری به نام routeProvider$ شناسایی میشود که خدمات مسیریابی را به ما ارائه میدهد. این سرویس به ما کمک میکند تا بتوانیم اتصال بین کنترلر ها، ویوها و آدرس URL جاری مرورگرها را به آسانی برقرار کنیم.

بهتر است کار را شروع کنیم . یک فایل JS ایجاد و سپس محتویات زیر را در آن قرار دهید : 

var myFirstRoute = angular.module('myFirstRoute', []);
  
myFirstRoute.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/pageOne', {
        templateUrl: 'templates/page_one.html',
        controller: 'ShowPage1Controller'
      }).
      when('/pageTwo', {
        templateUrl: 'templates/page_two.html',
        controller: 'ShowPage2Controller'
      }).
      otherwise({
        redirectTo: '/pageOne'
      });
}]);


myFirstRoute.controller('ShowPage1Controller', function($scope) {
    $scope.message = 'Content of page-one.html';
});
 
 
myFirstRoute.controller('ShowPage2Controller', function($scope) {
    $scope.message = 'Content of page-two.html';
});

در کدهای بالا ابتدا یک ماژول تعریف کرده ایم و سپس توسط ()config. تنظیمات مربوط به مسیریابی را انجام داده ایم. با استفاده از متدهای when. و otherwise. میتوانیم مسیرها را تعریف کنیم. برای هر مسیر دو پارامتر وجود دارد که اولین پارامتر نام مسیر و دومین پارامتر شامل 2 قسمت میشود که templateUrl  آن آدرسی که باز خواهد شد و controller نیز نام کنترلری که ویو را مدیریت میکند.

توسط otherwise میتوانیم مسیر پیشفرض را نیز تعریف کنیم تا درصورتی که مسیری با آدرس‌های بالای آن مطابقت نداشت به این آدرس منتقل شود.

در قطعه کد بالا همچنین دو مسیر با نام‌های pageOne/ و pageTwo/ تعریف کرده ایم که هر کدام به ترتیب به View‌های : templates/page_one.html و templates/page_two.html مرتبط شده اند. همچنین دو کنترلر برای مدیریت ویو‌ها نیز تعریف شده است.

زمانی که ما آدرس http://appname/#pageOne را در نوار آدرس مرورگر وارد میکنیم، Angular به صورت اتوماتیک آدرس URL را با تنظیماتی که ما در اینجا تعریف کرده ایم مطابقت میدهد و در صورت وجود چنین آدرسی ، view مربوطه را بارگزاری میکند و در این مثال نیز مطابق با تنظیمات بالا، صفحه‌ی templates/page_one.html برای ما بارگزاری و سپس کنترلر ShowPage1Controller را فراخوانی میکند ، جایی که ما منطق کار را در آن قرار میدهیم.

محتویات فایل main.html :  

<body ng-app="app">
  
    <div>
        <div>
        <div>
            <ul>
                <li><a href="#pageOne"> Show page one </a></li>
                <li><a href="#pageTwo"> Show page two </a></li>
            </ul>
        </div>
        <div>
            <div ng-view></div>
        </div>
        </div>
    </div>
  
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
    <script src="app.js"></script>
  
  </body>

در قطعه کد بالا دو لینک تعریف شده است که ویژگی href از علامت هش و نام صفحه تشکیل شده است. یکی از چیزهایی که شایان ذکر است ، دایرکتیو ng-view است. مکانی برای بارگزاری صفحات در آن.

ما میتوانیم این تگ را به سه شکل زیر نیز استفاده کنیم :

<div ng-view></div>
..
<ng-view></ng-view>
..
<div class="ng-view"></div>

محتویات صفحه  templates/page_one.html :

<h2>Page One</h2>
 
{{ message }}

محتویات صفحه templates/page_two.html :

<h2>Page Two</h2>
 
{{ message }}

حال اگر پروژه را اجرا کنید و به کنسول مرور گر خود نگاه کنید متوجه میشوید که مسیریاب از مسیر پیشفرض استفاده کرده است و صفحه‌ی page_one.html را به صورت ایجکسی فراخوانی کرده است :

و اگر روی لینک Show Page two کلیک کنید ، صفحه‌ی page_two.html نیز به صورت ایجکسی فراخوانی میشود.

دوباره بر روی لینک Show page one کلیک کنید. بله. هیچ درخواستی به سمت سرور ارسال نشد و صفحه‌ی page_one.html به خوبی نمایش داده شد. یکی از مزیت‌های سیستم مسیریابی قابلیت کش کردن صفحات است تا در صورت فراخوانی مجدد، درخواستی به سمت سرور ارسال نشود و خیلی سریع به شما نمایش داده شود.

مثال این مطلب : RouteExample.zip  

ادامه دارد ...

مطالب
ساختارهای داده‌ی توکار ES 6
در ES 5 تنها آرایه (Array) و آبجکت (Object) را به عنوان ساختار داده‌ایی، به صورت توکار در اختیار داریم.
Array یک کالکشن مبتنی بر ایندکس است. همچنین می‌توان هر نوع مقداری را در آن ذخیره کرد:
var collection = ['a', 1, /3/, {}];
یعنی هر کدام از اعضای آرایه می‌توانند جنس متفاوتی داشته باشند. همانطور که در کد فوق مشاهده می‌کنید اعضای آرایه به ترتیب از کاراکتر، عدد، عبارت با قاعده و در نهایت یک شیء خالی تشکیل شده است. همانطور که عنوان شد آرایه‌ها در جاوا اسکریپت همانند دیگر زبان‌های برنامه‌نویسی مبتنی بر ایندکس هستند، یعنی می‌توان براساس ایندکس به هر کدام از اعضای آرایه دسترسی داشت:
collection[0];
می‌توان از پراپرتی length نیز برای دریافت سایز آرایه استفاده کرد:
collection.length
همانطور که در مثال ابتدای بحث مشاهده کردید، آرایه‌ها در جاوا اسکریپت توسط سینتکس [] قابل تعریف هستند. تعدادی تابع توکار برای کار با آرایه‌ها موجود است که در اینجا می‌توانید لیست کامل آنها را مشاهد نمائید. همچنین می‌توانید از کتابخانه‌های دیگری مانند Underscore.js که در واقع هدف آن‌ها افزودن یکسری قابلیت‌ها به جاوا اسکریپت است، استفاده کنید:
var numbers = [1, 2, 3];

_.each(numbers, function (num) {
    write(num);
});
در ES 6 تعدادی تابع جدید به Array اضافه شده که کار با آرایه‌ها را ساده‌تر کرده است. در ادامه تعدادی از این توابع را بررسی خواهیم کرد.
تابع find
این تابع از ورودی، یک callback را گرفته و نتایج یافته شده را در خروجی برمی‌گرداند:
var ary = [1, 5, 10];
var match = ary.find(item => item > 8);
تابع findIndex
این تابع مشابه تابع find عمل می‌کند با این تفاوت که در خروجی ایندکس عنصر یافته شده را برمی‌گرداند:
var match = ary.findIndex(item => item > 8);
تابع fill
از این تابع می‌توان جهت مقداردهی اعضای آرایه با پارامتر موردنظر استفاده کرد:
ary.find('a');
// خروجی ["a", "a", "a"]
لازم به ذکر است، به این تابع می‌توانیم دو پارامتر دیگر را نیز پاس دهیم:
var ary = [1, 5, 10, 5, 6];
ary.fill('a', 2, 3)
در کد فوق پارامتر دوم یعنی نقطه شروع و پارامتر سوم یعنی نقطه پایان:
// خروجی
[ 1, 5, "a", 5, 6 ]
تابع copyWithin
با کمک این تابع می‌توانیم قسمتی از یک آرایه را کپی کرده و در محل دیگری از آرایه ذخیره کنیم:
[1, 2, 3, 4, 5].copyWithin(0, 3);
کد فوق از نقطه‌ی سوم شروع به کپی کردن آیتم‌ها کرده و آنها را در موقعیت صفرم آرایه به بعد قرار می‌دهد. در نتیجه خروجی آن به این صورت است:
// [4, 5, 3, 4, 5]
لازم به ذکر است که یک پارامتر سوم را نیز می‌توانیم جهت تعیین نقطه‌ی پایان به تابع فوق اضافه کنیم:
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);
// خروجی 
// [4, 2, 3, 4, 5]
در ES 6 علاوه بر سینتکس literal می‌توان از سازنده‌ی کلاس Array نیز جهت تعریف آرایه‌ها، استفاده کرد:
var ary = new Array(1, 2);
کد فوق یک آرایه را با دو مقدار 1 و 2 ایجاد می‌کند. اگر بخواهیم یک آیتم جدید را به آرایه‌ی فوق اضافه کنیم، باید آن را نیز به پارامترهای فوق اضافه کرد:
var ary = new Array(1, 2, 3);
ممکن است فکر کنید توسط کد زیر آرایه‌ایی تنها با یک آیتم برای ما ایجاد خواهد شد:
var ary = new Array(3);
در واقع کد فوق یک آرایه با اندازه‌ی سه و محتوای undefined را برای شما ایجاد خواهد کرد. در نتیجه برای ایجاد آرایه‌ایی با یک آیتم و مقدار 3 باید از متد Of کلاس Array استفاده کنیم:
var Ofary = Array.of(3);

Set
Set یک ساختار داده‌ایی جدید در ES 6 است. این ساختار داده‌ایی امکان تعریف کالکشنی از مقادیر را به صورت unique، در اختیارمان قرار می‌دهد. برخلاف آرایه‌ها مقادیر درون Set نمی‌تواند یکسان باشند. در کد زیر نحوه‌ی ایجاد یک Set نشان داده شده است:
var set = new Set();
set.add(1);
set.add(2);
set.add(3);
console.log(set.size); // logs 3
 Set به صورت یک مجموعه‌ی  Iterable است یعنی می‌توان اعضای این مجموعه را آیتم به آیتم پیمایش کرد. همانطور که در کد فوق مشاهده می‌کنید توسط add می‌توانیم آیتم جدیدی را به مجموعه اضافه کنیم. همچنین اگر مایل بودید می‌توانید مجموعه را توسط یک آرایه به صورت زیر نیز مقداردهی کنید:
var set = new Set([1, 2, 3]);
console.log(set.size); // logs 3
از توابع has, delete, clear نیز به ترتیب می‌توان جهت خالی کردن مجموعه، حذف یک آیتم از مجموعه و بررسی یک آیتم در مجموعه استفاده کرد:
var set = new Set();
set.has(1); // false
set.add(1);
set.has(1); // true
set.clear();
set.has(1); // false
set.add(1);
set.add(2);
set.size;   // 2
set.delete(2);
set.size;   // 1
از تابع feorach نیز می‌توانیم برای حرکت بین آیتم‌های مجموعه استفاده کنیم:
var set = new Set();
set.add('Vahid');
set.add('Sirwan');

var i = 0;
set.forEach(item => i++);
console.log(i);
همچنین از سینتکس for...of نیز می‌توان برای پیمایش مجموعه استفاده کرد:
var set = new Set();
set.add('Vahid');
set.add('Sirwan');

var i = 0;
for(let item of set) {
    i++;
}
console.log(i);
Set دارای یک تابع دیگر با نام entries است. با کمک این تابع یک iterator از مجموعه برگردانده خواهد شد که با کمک تابع next می‌توان به عناصر بعدی مجموعه دسترسی پیدا کرد:
var set = new Set();
set.add("Sirwan");
set.add(1);
set.add("Afifi");

var setIter = set.entries();

console.log(setIter.next().value); // ["Sirwan", "Sirwan"]
console.log(setIter.next().value); // [1, 1]
console.log(setIter.next().value); // ["Afifi", "Afifi"]

Map
برخلاف Set که یک مجموعه از مقادیر (values) است، Map یک مجموعه از کلید/مقدار (key/value) می‌باشد. در اینجا نیز کلیدها باید unique باشند. همچنین می‌توان از هر نوعی برای کلید استفاده کرد. برای افزودن یک مقدار به این مجموعه باید از تابع set استفاه کنیم:
var map = new Map();
map.set('name', 'Sirwan');

map.get('name'); // Sirwan
همانطور که مشاهده می‌کنید توسط تابع get نیز می‌توانیم با استفاده از کلید، به مقدار آن دسترسی داشته باشیم. همچنین می‌توانیم آرایه‌ایی از آرایه‌ها را به عنوان کلید در یک Map ذخیره کنیم:
var map = new Map([['name', 'Sirwan'], ['age', 27]]);
map.has('age'); // true
map.get('age'); // 27
map.get('name'); // Sirwan

نکته‌ایی که در استفاده از Map باید به آن دقت کنید این است که در اینجا هیچ تبدیل نوعی را بر روی کلیدها نداریم:
var map = new Map();

map.set(1, true);
map.has("1"); // false

map.set("1", true);
map.has("1"); // true
همانند Set برای Map نیز می‌توانیم از توابع delete و clear استفاده کنیم. برای استفاده از foreach باید برای callback دو پارامتر را ارائه دهیم. یکی برای value و دیگری برای key:
var map = new Map([['name', 'Sirwan'], ['age', 27]]);
var i = 0;
map.forEach(function (value, key) {
    i++;
});
console.log(i); // log 2
برای سینتکس for...of نیز می‌توانیم به اینصورت عمل کنیم:
for (var [key, value] of map) {
    i++;
}
شاید بپرسید که همین کار را می‌توان با استفاده از آرایه‌ها نیز انجام داد و چه نیازی به یک ساختار داده‌ایی جدید است؟
اگر بخواهید Map را با استفاده از آرایه‌ها شبیه‌سازی کنید باید از Associative Arrays استفاده کنید؛ به زبان ساده در این‌حالت به جای استفاده از عدد به جای ایندکس می‌توان رشته‌ها نیز استفاده کرد. به عنوان مثال کد زیر را در نظر بگیرید:
var newArray = new Array();
newArray["name"] = "Sirwan";
newArray["lastName"] = "Afifi";
در اینجا ایندکس‌ها به ترتیب name و lastName هستند و به عنوان کلید مورد استفاده قرار می‌گیرند. کلیدها نیز به مقادیر "Sirwan" و "Afifi" مپ شده‌اند. حالت فوق شبیه به یک دیکشنری عمل می‌کند. اما همانطور که عنوان شد در اینجا کلید به صورت رشته‌ایی است و نمی‌توان از اشیاء به عنوان کلید استفاده کرد؛ زیرا در نهایت تبدیل به رشته خواهند شد:
let user1 = { name: "Vahid" };
let user2 = { name: "Sirwan" };

let result = {};
result[user1] = 10;
result[user2] = 20;

console.log(result[user1]); // logs 20
console.log(result[user2]); // logs 20
در کد فوق هر کدام از شیء‌ها را به عنوان کلید در نظر گرفته‌ایم و برای هر کدام مقادیر 10 و 20 را ست کرده‌ایم. اما خروجی هر کدام 20 است؛ در حالیکه باید به ترتیب عدد 10 و سپس عدد 20 در خروجی نمایش داده شود. دلیل آن نیز کاملاً مشخص است زیرا اگر در جاوا اسکریپت برای یک شیء تابع toString را فراخوانی کنیم، مقدار "[object object]" در خروجی نمایش داده خواهد شد. در نتیجه در کد فوق در واقع هر بار ایندکس "[object object]" را به‌روز رسانی کرده‌ایم:
result["[object object]"] = 10;
result["[object object]"] = 20;

console.log(result["[object object]"]); // logs 20
console.log(result["[object object]"]); // logs 20

WeakMap and WeakSet
فرض کنید درون DOM سه عنصر div دارید و می‌خواهید این سه div را درون یک Set ذخیره کنید:

در این‌حالت آیتم‌های درون Set ارجاع مستقیمی را به عناصر موجود در DOM دارند. اکنون حالتی را در نظر بگیرید که بخواهیم یکی از عناصر موجود درون DOM را حذف کنیم. در اینحالت آیتم درون Set که به این عنصر اشاره دارد هنوز حذف نشده است و همچنان ارجاعی را به آن عنصر دارد. بنابراین تا زمانیکه آیتم از Set حذف نشود Garbage Collector نمی‌تواند حافظه‌ی اختصاص داده شده را مجدداً بازیابی کند. در نتیجه استفاده از Set و یا Map در چنین سناریوهایی منجر به نشتی حافظه خواهد شد. برای حل این مشکل می‌توانیم از WeakMap و یا WeakSet استفاده کنیم. در این‌حالت WeakMap و WeakSet ارجاع مستقیمی به اشیایی که به آنها اضافه می‌شوند، ندارند. در نتیجه GC به راحتی می‌تواند حافظه‌ی اختصاص داده شده را بعد از حذف اشیاء بازیابی کند.
صرف‌نظر از رفع مشکل حافظه، WeakMap و WeakSet شبیه به Map و Set عمل می‌کنند، اما یکسری محدودیت‌هایی در استفاده از آنها وجود دارد:
  • WeakMap و WeakSet فاقد پراپرتی‌های size, entries, values و متد foreach هستند.
  • WeakMap همچنین فاقد keys است.
نظرات مطالب
مقایسه بین حلقه های تکرار (Lambda ForEach و for و foreach)
"این آزمایشات رو اگر در هر سیستم دیگر با هر Config اجرا کنید نتیجه کلی تغییر نخواهد کرد و فقط از نظر زمان اجرا تفاوت خواهیم داشت نه در نتیجه کلی." 
این مطلب لزوما صحیح نیست. یک بنچمارک میتونه تو مجموعه سخت افزارهای مختلف، نتایج کاملا متفاوتی داشته باشه. مثلا سوالی در همین زمینه آقای شهروز جعفری تو StackOverflow پرسیدن که در جوابش دو نفر نتایج متفاوتی ارائه دادن.
معمولا برای بیان نتایج تستهای بنچمارک ابتدا مشخصات سخت افزاری ارائه میشه مخصوصا وقتیکه نتایج دقیق (و نه کلی) نشون داده میشه. مثل همین نتایج دقیق زمانهای اجرای حلقه‌ها.
نکته ای که من درکامنتم اشاره کردم صرفا درباره تست "سرعت اجرای" انواع حلقه‌ها بود که ممکنه با تست کارایی حلقه‌ها در اجرای یک کد خاص فرق داشته باشه.
نکته دیگه هم اینکه نمیدونم که آیا شما از همون متد Console.WriteLine در حلقه‌ها برای اجرای تستون استفاده کردین یا نه. فقط باید بگم که به خاطر مسائل و مشکلات مختلفی که استفاده از این متد به همراه داره، به نظر من بکارگیری اون تو این جور تست‌ها اصلا مناسب نیست و باعث دور شدن زیاد نتایج از واقعیت میشه. مثلا من تست کردم و هر دفعه یه نتیجه‌ای می‌داد که نمیشه بر اساس اون نتیجه‌گیری کرد. 

مورد دیگه ای هم که باید اضافه کنم اینه که بهتر بود شما کد کامل تست خودتون رو هم برای دانلود میذاشتین تا دیگران هم بتونن استفاده کنن. اینجوری خیلی بهتر میشه نتایج مختلف رو با هم مقایسه کرد. این مسئله برای تستای بنچمارک نسبتا رایج هست. مثل کد زیر که من آماده کردم:
static void Main(string[] args)
{
  //Action<int> func = Console.WriteLine;
  Action<int> func = number => number++;
  do
  {
    try
    {
      Console.Write("Iteration: ");
      var iterations = Convert.ToInt32(Console.ReadLine());
      Console.Write("Loop Type (for:0, foreach:1, List.ForEach:2, Array.ForEach:3): ");
      var loopType = Console.ReadLine();
      switch (loopType)
      {
        case "0":
          Console.WriteLine("FOR loop test for {0} iterations", iterations.ToString("0,0"));
          TestFor(iterations, func);
          break;
        case "1":
          Console.WriteLine("FOREACH loop test for {0} iterations", iterations.ToString("0,0"));
          TestForEach(iterations, func);
          break;
        case "2":
          Console.WriteLine("LIST.FOREACH test for {0} iterations", iterations.ToString("0,0"));
          TestListForEach(iterations, func);
          break;
        case "3":
          Console.WriteLine("ARRAY.FOREACH test for {0} iterations", iterations.ToString("0,0"));
          TestArrayForEach(iterations, func);
          break;
      }
    }
    catch (Exception ex)
    {
      Console.WriteLine(ex);
    }
    Console.Write("Continue?(Y/N)");
    Console.WriteLine("");
  } while (Console.ReadKey(true).Key != ConsoleKey.N);

  Console.WriteLine("Press any key to exit");
  Console.ReadKey();
}


static void TestFor(int iterations, Action<int> func)
{
  StartupTest(func);

  var watch = Stopwatch.StartNew();
  for (int i = 0; i < iterations; i++)
  {
    func(i);
  }
  watch.Stop();
  ShowResults("for loop test: ", watch);
}

static void TestForEach(int iterations, Action<int> func)
{
  StartupTest(func);
  var list = Enumerable.Range(0, iterations);

  var watch = Stopwatch.StartNew();
  foreach (var item in list)
  {
    func(item);
  }
  watch.Stop();
  ShowResults("foreach loop test: ", watch);
}

static void TestListForEach(int iterations, Action<int> func)
{
  StartupTest(func);
  var list = Enumerable.Range(0, iterations).ToList();

  var watch = Stopwatch.StartNew();
  list.ForEach(func);
  watch.Stop();
  ShowResults("list.ForEach test: ", watch);
}

static void TestArrayForEach(int iterations, Action<int> func)
{
  StartupTest(func);
  var array = Enumerable.Range(0, iterations).ToArray();

  var watch = Stopwatch.StartNew();
  Array.ForEach(array, func);
  watch.Stop();
  ShowResults("Array.ForEach test: ", watch);
}

static void StartupTest(Action<int> func)
{
  // clean up
  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();

  // warm up
  func(0);
}
static void ShowResults(string description, Stopwatch watch)
{
  Console.Write(description);
  Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
}
قبل از اجرای تست بهتره برنامه رو برای نسخه Release بیلد کنیم. ساده‌ترین روشش در تصویر زیر نشون داده شده:

پس از این تغییر و بیلد پروژه نتایج رو مقایسه میکنیم. نتایج اجرای این تست در همون سیستمی که قبلا تستای StringBuilder و Microbenchmark رو انجام دادم (یعنی لپ تاپ msi GE 620 با Core i7-2630QM) بصورت زیر:

البته نتایج این تستها مطلق نیستن. نکاتی که در کامنت قبلی اشاره کردم از عوامل تاثیرگذار هستن.
موفق باشین.
مطالب
تهیه یک DynamicXml برای خواندن اطلاعات فایل Xml با استفاده از انقیاد پویا در سی‌شارپ

در فریمورک NET. ابزارهای مختلفی برای کار با داده‌های XML در نظر گرفته شده‌است که بعد از نسخه 3.5 آن، انتخاب اول LINQ to XML می باشد. در این مطلب قصد داریم API ای را برای خواندن اطلاعات فایل‌های XML با استفاده از LINQ to XML و انقیاد پویا در سی‌شارپ (Dynamic Binding) تهیه کنیم.


راه حل اول: استفاده از ExpandoObject

public static class ExpandoXml
{
    public static dynamic AsExpando(this XDocument document)
    {
        return CreateExpando(document.Root);
    }

    private static dynamic CreateExpando(XElement element)
    {
        var result = new ExpandoObject() as IDictionary<string, object>;
        if (element.Elements().Any(e => e.HasElements))
        {
            var list = new List<ExpandoObject>();
            result.Add(element.Name.ToString(), list);
            foreach (var childElement in element.Elements())
            {
                list.Add(CreateExpando(childElement));
            }
        }
        else
        {
            foreach (var leafElement in element.Elements())
            {
                result.Add(leafElement.Name.ToString(), leafElement.Value);
            }
        }
        return result;
    }
}

در تکه کد بالا از طریق متد CreateExpando به صورت بازگشتی ابتدا بررسی می‌شود که آیا عنصر جاری دارای عناصری می‌باشد و همچنین آیا آنها دارای فرزند می‌باشند یا خیر؛ در صورت برقراری شرط، نتیجه‌ی اجرای متد CreateExpando بر روی تک تک عناصر فرزند را درون لیستی از ExpandoObject قرار داده و سپس آن لیست نیز به عنوان Value عنصر جاری در نظر گرفته می‌شود. در صورت عدم برقراری شرط مذکور، مقادیر مربوط به عناصر فرزند را در قالب یک ExpandoObject به عنوان خروجی بازگشت خواهد داد.


راه حل دوم: استفاده از DynamicObject

برای این منظور کلاس زیر را در نظر بگیرید:
    public class DynamicXml : DynamicObject, IEnumerable
    {
        private readonly dynamic _xml;
        public DynamicXml(string fileName)
        {
            _xml = XDocument.Load(fileName);
        }

        public DynamicXml(dynamic xml)
        {
            _xml = xml;
        }

        public IEnumerator GetEnumerator()
        {
            foreach (var item in _xml.Elements())
            {
                yield return new DynamicXml(item);
            }
        }

        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            var xml = _xml.Element(binder.Name);
            if (xml != null)
            {
                result = new DynamicXml(xml);
                return true;
            }

            var attribute = _xml.Attribute(binder.Name);
            if (attribute != null)
            {
                result = new DynamicXml(attribute);
                return true;
            }

            result = null;
            return false;
        }

        public static implicit operator string(DynamicXml xml)
        {
            return xml._xml.Value;
        }
    }

کلاس DynamicXml از طریق سازنده اول، نام فایل را دریافت کرده و از طریق LINQ to XML با استفاده از متد Load کلاس XDocument، فایل مورد نظر بارگذاری شده و درون فیلدی به نام xml_ از نوع dynamic نگه داشته می‌شود. کار بعدی، بازنویسی متد TryGetMember می‌باشد. در بدنه بازنویسی شده این متد ابتدا بررسی می‌شود که آیا با نام خصوصت درخواست شده عنصری در داده XML وجود دارد یا خیر؛ در صورت موجود بودن، پارامتر result با یک  وهله جدید از DynamicXml مقدار دهی می‌شود که عنصر یافت شده از طریق سازنده دوم، به عنوان داده xml برای مقدار دهی فیلد xml_ به عنوان آرگومان ارسال می‌شود. در صورت عدم وجود عنصر مذکور، بدنبال خصوصیتی با آن نام بوده و در صورت یافت شدن، باز به عنوان یک وهله DynamicXml برای مقدار دهی result استفاده می‌شود.

در ادامه برای نسبت دادن یک وهله از DynamicXml به یک متغیر string و دستیابی به مقدار یک عنصر که از طریق خصوصیت، درخواست می‌شود نیاز است تا اپراتور ضمنی string را نیز برای کلاس بالا نظر بگیریم. همچنین برای ایجاد امکان پیمایش برروی عناصر فرزند از طریق foreach، لازم است واسط IEnumerable را نیز پیاده سازی کرده باشیم.


طریقه استفاده

    class Program
    {
        static void Main(string[] args)
        {
            var doc1 = XDocument.Load("Employees.xml");
            
            foreach (var element in doc1.Element("Employees").Elements("Employee"))
            {
                Console.WriteLine(element.Element("FirstName").Value);
            }

            dynamic doc2 = XDocument.Load("Employees.xml").AsExpando();
            foreach (var employee in doc2.Employees)
            {
                Console.WriteLine(employee.FirstName);
            }

            dynamic doc3 = new DynamicXml("Employees.xml");
            foreach (var employee in doc3.Employees)
            {
                Console.WriteLine(employee.FirstName);
                Console.WriteLine(employee.Id);
            }
        }
    }
و فایل Employees.xml
<?xml version="1.0" encoding="utf-8" ?>
<Employees>
    <Employee Id="1">
        <FirstName>
            Employee1
        </FirstName>
    </Employee>
     <Employee Id="2">
        <FirstName>
            Employee2
        </FirstName>
    </Employee>
     <Employee Id="3">
        <FirstName>
            Employee3
        </FirstName>
    </Employee>
     <Employee Id="4">
        <FirstName>
            Employee4
        </FirstName>
    </Employee>
</Employees>

مطالب
پیاده سازی Remote Validation در Blazor
فرم‌های Blazor به همراه پشتیبانی از ویژگی Remote که به همراه ASP.NET Core ارائه می‌شود، نیستند. هرچند می‌توان در حین ارسال فرم به سرور، نتیجه‌ی اعتبارسنجی از راه دور و سمت سرور را به کاربر نمایش داد، اما تجربه‌ی کاربری آن در حد Remote validation نیست. یعنی می‌خواهیم در حین ورود اطلاعات و یا انتقال focus به کنترل دیگری، اعتبارسنجی سمت سرور صورت گیرد و نه فقط در زمان ارسال کل اطلاعات به سرور، در پایان کار. در این مطلب روشی را جهت پیاده سازی این عملیات بررسی خواهیم کرد.


ایجاد یک پروژه‌ی ابتدایی Blazor WASM

پروژه‌ای را که در این مطلب تکمیل خواهیم کرد، از نوع Blazor WASM هاست شده‌است. بنابراین در پوشه‌ی فرضی BlazorAsyncValidation، دستور «dotnet new blazorwasm --hosted» را صادر می‌کنیم تا ساختار ابتدایی پروژه که به همراه یک کلاینت Blazor WASM و یک سرور ASP.NET Core Web API است، تشکیل شود. از قسمت Web API، برای پیاده سازی اعتبارسنجی سمت سرور استفاده خواهیم کرد.


مدل ثبت نام برنامه

مدل ثبت نام برنامه تنها از یک خاصیت نام تشکیل شده و در پروژه‌ی Shared قرار می‌گیرد تا هم در کلاینت و هم در سرور قابل استفاده باشد:
using System.ComponentModel.DataAnnotations;

namespace BlazorAsyncValidation.Shared;

public class UserDto
{
    [Required] public string Name { set; get; } = default!;
}
این نام است که می‌خواهیم عدم تکراری بودن آن‌را حین ثبت نام در سمت سرور، بررسی کنیم. به همین جهت کنترلر API زیر را برای آن تدارک خواهیم دید.


کنترلر API ثبت نام برنامه

کنترلر زیر که در پوشه‌ی BlazorAsyncValidation\Server\Controllers قرار می‌گیرد، منطق بررسی تکراری نبودن نام دریافتی از برنامه‌ی کلاینت را شبیه به منطق remote validation استاندارد MVC، پیاده سازی می‌کند که در نهایت یک true و یا false را باز می‌گرداند.
در اینجا خروجی بازگشت داده کاملا در اختیار شما است و نیازی نیست تا حتما ارتباطی با MVC داشته باشد؛ چون مدیریت سمت کلاینت بررسی آن‌را خودمان انجام خواهیم داد و نه یک کتابخانه‌ی از پیش نوشته شده و مشخص.
using BlazorAsyncValidation.Shared;
using Microsoft.AspNetCore.Mvc;

namespace BlazorAsyncValidation.Server.Controllers;

[ApiController, Route("api/[controller]/[action]")]
public class RegisterController : ControllerBase
{
    [HttpPost]
    public IActionResult IsUserNameUnique([FromBody] UserDto userModel)
    {
        if (string.Equals(userModel?.Name, "Vahid", StringComparison.OrdinalIgnoreCase))
        {
            return Ok(false);
        }

        return Ok(true);
    }
}

غنی سازی فرم استاندارد Blazor جهت انجام Remote validation



اگر بخواهیم از EditForm استاندارد Blazor در حالت متداول آن و بدون هیچ تغییری استفاده کنیم، مانند مثال زیر که InputText متصل به خاصیت Name مربوط به Dto برنامه را نمایش می‌دهد:
@page "/"

<PageTitle>Index</PageTitle>

<h2>Register</h2>

<EditForm EditContext="@EditContext" OnValidSubmit="DoSubmitAsync">
    <DataAnnotationsValidator/>
    <div class="row mb-2">
        <label class="col-form-label col-lg-2">Name:</label>
        <div class="col-lg-10">
            <InputText @bind-Value="Model.Name" class="form-control"/>
            <ValidationMessage For="@(() => Model.Name)"/>
        </div>
    </div>

    <button class="btn btn-secondary" type="submit">Submit</button>
</EditForm>
 تنها رخ‌دادی که در اختیار ما قرار می‌گیرد، رخ‌داد submit (در حالت موفقیت اعتبارسنجی سمت کلاینت و یا تنها submit معمولی) است. بنابراین برای دسترسی به رخ‌دادهای بیشتر EditForm، نیاز است با EditContext آن کار کنیم:
public partial class Index
{
    private const string UserValidationUrl = "/api/Register/IsUserNameUnique";

    private ValidationMessageStore? _messageStore;
    [Inject] private HttpClient HttpClient { set; get; } = default!;
    private EditContext? EditContext { set; get; }

    private UserDto Model { get; } = new();
به همین جهت EditContext را در سطح کامپوننت جاری تعریف کرده و همچنین سرویس HttpClient را جهت ارسال اطلاعات Name و دریافت پاسخ true/false از سرور و یک ValidationMessageStore را برای نگهداری تعاریف خطاهای سفارشی که قرار است در فرم نمایش داده شوند، معرفی می‌کنیم.
ValidationMessageStore به همراه متد Add است و اگر به آن نام فیلد مدنظر را به همراه پیامی، اضافه کنیم، این اطلاعات را به صورت خطای اعتبارسنجی توسط کامپوننت ValidationMessage نمایش می‌دهد.

محل مقدار دهی اولیه‌ی این اشیاء نیز در روال رویدادگردان OnInitialized به صورت زیر است:
    protected override void OnInitialized()
    {
        EditContext = new EditContext(Model);
        _messageStore = new ValidationMessageStore(EditContext);
        EditContext.OnFieldChanged += (sender, eventArgs) =>
        {
            var fieldIdentifier = eventArgs.FieldIdentifier;
            _messageStore?.Clear(fieldIdentifier);
            _ = InvokeAsync(async () =>
            {
                var errors = await OnValidateFieldAsync(fieldIdentifier.FieldName);
                if (errors?.Any() != true)
                {
                    return;
                }

                foreach (var error in errors)
                {
                    _messageStore?.Add(fieldIdentifier, error);
                }

                EditContext.NotifyValidationStateChanged();
            });
            StateHasChanged();
        };
        EditContext.OnValidationStateChanged += (sender, eventArgs) => StateHasChanged();
        EditContext.OnValidationRequested += (sender, eventArgs) => _messageStore?.Clear();
    }
در اینجا ابتدا یک نمونه‌ی جدید از EditContext، بر اساس Model فرم، تشکیل می‌شود. سپس ValidationMessageStore سفارشی که قرار است خطاهای اعتبارسنجی remote ما را نمایش دهد نیز نمونه سازی می‌شود. در ادامه امکان دسترسی به رخ‌دادهای OnFieldChanged ، OnValidationStateChanged و OnValidationRequested را خواهیم داشت که در حالت معمولی کار با EditForm میسر نیستند.
برای مثال اگر فیلدی تغییر کند، رویداد OnFieldChanged صادر می‌شود. در همینجا است که کار فراخوانی متد OnValidateFieldAsync که در ادامه معرفی می‌شود را انجام می‌دهیم تا کار اعتبارسنجی Async سمت سرور را انجام دهد. اگر نتیجه‌ای به همراه آن بود، توسط messageStore به صورت یک خطای اعتبارسنجی نمایش داده خواهد شد و همچنین EditContext نیز با فراخوانی متد NotifyValidationStateChanged، وادار به به‌روز رسانی وضعیت اعتبارسنجی خود می‌گردد.

متد سفارشی OnValidateFieldAsync که کار اعتبارسنجی سمت سرور را انجام می‌دهد، به صورت زیر تعریف شده‌است:
    private async Task<IList<string>?> OnValidateFieldAsync(string fieldName)
    {
        switch (fieldName)
        {
            case nameof(UserDto.Name):
                var response = await HttpClient.PostAsJsonAsync(
                    UserValidationUrl,
                    new UserDto { Name = Model.Name });
                var responseContent = await response.Content.ReadAsStringAsync();
                if (string.Equals(responseContent, "false", StringComparison.OrdinalIgnoreCase))
                {
                    return new List<string>
                    {
                        $"`{Model.Name}` is in use. Please choose another name."
                    };
                }

                // TIP: It's better to use the `DntDebounceInputText` component for this case to reduce the network round-trips.

                break;
        }

        return null;
    }
در اینجا درخواستی به سمت آدرس api/Register/IsUserNameUnique ارسال شده و سپس بر اساس مقدار true و یا false آن، پیامی را بازگشت می‌دهد. اگر نال را بازگشت دهد یعنی مشکلی نبوده.

یک نکته: InputText استاندارد در حالت معمول آن، پس از تغییر focus به یک کنترل دیگر، سبب بروز رویداد OnFieldChanged می‌شود و نه در حالت فشرده شدن کلیدها. به همین جهت اگر برنامه پیوستی را می‌خواهید آزمایش کنید، نیاز است فقط focus را تغییر دهید و یا یک کنترل سفارشی را برای اینکار توسعه دهید.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorAsyncValidation.zip