نظرات مطالب
Blazor 5x - قسمت هشتم - مبانی Blazor - بخش 5 - تامین محتوای نمایشی کامپوننت‌های فرزند توسط کامپوننت والد
یک نکته‌ی تکمیلی: امکان داشتن متدهایی با خروجی تگ‌دار در برنامه‌های Blazor (یا تعریف کامپوننت‌های پویا)

اگر با React کار کرده باشید، یک چنین کدهایی اساس آن‌را تشکیل می‌دهند:
import React from "react";

const Rentals = () => {
  return <h1>Rentals</h1>;
};

export default Rentals;
در اینجا کامپوننتی به نام Rentals تعریف شده‌است که خروجی آن ... یک تگ HTML ای است و برای تشکیل آن نیازی به استفاده از "" و چسباندن رشته‌ها نبوده‌است. دقیقا شبیه به یک چنین کاری را می‌توان در برنامه‌های Blazor نیز انجام داد که به آن «Razor template syntax» و یا «templated components» هم گفته می‌شود:
@page "/razor"

@template
@ItemTemplate(emp)

@code {

    RenderFragment template = @<p>The time is @DateTime.Now.</p>;
    RenderFragment<Employee> ItemTemplate = (item) => @<p>Employee name is @item.Name.</p>;

    Employee emp = new Employee { Name = "Test" };

    public class Employee
    {
        public string Name;
    }
}
در اینجا همانطور که مشاهده می‌کنید، امکان انتساب یک قالب HTML ای شروع شده‌ی با @ به یک RenderFragment وجود دارد که حتی می‌تواند جنریک هم باشد و وهله‌ای از این شیء جنریک را به صورت یک lambda expression دریافت کند. روش درج آن‌ها را در صفحه نیز مشاهده می‌کنید که همانند فراخوانی متدها است و برای نمونه ItemTemplate جنریک، وهله‌ای از فیلد emp تعریف شده‌ی در قسمت code را دریافت کرده و در صفحه نمایش می‌دهد.
یا حتی می‌توان از RenderFragment برای وهله سازی پویای یک کامپوننت مانند SurveyPrompt، مقدار دهی خاصیت Title آن و درج آن در صفحه به صورت زیر هم استفاده کرد:
 @page "/"
  
 @CreateDynamicComponent()

 @code {
     RenderFragment CreateDynamicComponent() => builder =>
     {
         builder.OpenComponent(0, typeof(SurveyPrompt));
         builder.AddAttribute(1, "Title", "Some title");
         builder.CloseComponent();
     };
 }
مطالب
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
}

مطالب
پیاده سازی Unobtrusive Ajax در ASP.NET Core 1.0
پیاده سازی Unobtrusive Ajax را در ASP.NET MVC 5.x، می‌توانید در مطلب «ASP.NET MVC #21» مطالعه کنید. HTML Helpers مرتبط با Ajax، به طور کامل از ASP.NET Core 1.0 حذف شده‌اند. اما این مورد به این معنا نیست که نمی‌توان Unobtrusive Ajax را در ASP.NET Core که تمرکزش بیشتر بر روی Tag Helpers جدید هست تا HTML Helpers قدیمی، پیاده سازی کرد.


Unobtrusive Ajax چیست؟

در حالت معمولی، با استفاده از متد ajax جی‌کوئری، کار ارسال غیرهمزمان اطلاعات، به سمت سرور صورت می‌گیرد. چون در این روش کدهای جی‌کوئری داخل صفحات برنامه‌های ما قرار می‌گیرند، به این روش، «روش چسبنده» می‌گویند. اما با استفاده از افزونه‌ی «jquery.unobtrusive-ajax.min.js» مایکروسافت، می‌توان این کدهای چسبنده را تبدیل به کدهای غیرچسنبده یا Unobtrusive کرد. در این حالت، پارامترهای متد ajax، به صورت ویژگی‌ها (attributes) به شکل data-ajax به المان‌های مختلف صفحه اضافه می‌شوند و به این ترتیب، افزونه‌ی یاد شده به صورت خودکار با یافتن مقادیر ویژگی‌های data-ajax، این المان‌ها را تبدیل به المان‌های ای‌جکسی می‌کند. در این حالت به کدهایی تمیزتر و عاری از متدهای چسبنده‌ی ajax قرار گرفته‌ی در داخل صفحات وب خواهیم رسید.
روش طراحی Unobtrusive را در کتابخانه‌های معروفی مانند بوت استرپ هم می‌توان مشاهده کرد.


پیشنیازهای فعال سازی Unobtrusive Ajax در ASP.NET Core 1.0

توزیع افزونه‌ی «jquery.unobtrusive-ajax.min.js» مایکروسافت، از طریق bower صورت می‌گیرد که پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودی‌های کاربران» با آن آشنا شدیم. در اینجا نیز برای دریافت آن، تنها کافی است فایل bower.json را به نحو ذیل تکمیل کرد:
{
  "name": "asp.net",
  "private": true,
  "dependencies": {
   "bootstrap": "3.3.6",
   "jquery": "2.2.0",
   "jquery-validation": "1.14.0",
   "jquery-validation-unobtrusive": "3.2.6",
   "jquery-ajax-unobtrusive": "3.2.4"
  }
}
و پس از آن فایل bundleconfig.json مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 21 - بررسی تغییرات Bundling و Minification» یک چنین شکلی را پیدا می‌کند:
[
  {
   "outputFileName": "wwwroot/css/site.min.css",
   "inputFiles": [
    "bower_components/bootstrap/dist/css/bootstrap.min.css",
    "content/site.css"
   ]
  },
  {
   "outputFileName": "wwwroot/js/site.min.js",
   "inputFiles": [
    "bower_components/jquery/dist/jquery.min.js",
    "bower_components/jquery-validation/dist/jquery.validate.min.js",
    "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
    "bower_components/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js",
    "bower_components/bootstrap/dist/js/bootstrap.min.js"
   ],
   "minify": {
    "enabled": true,
    "renameLocals": true
   },
   "sourceMap": false
  }
]
در اینجا فایل‌های css و اسکریپت مورد نیاز برنامه، به ترتیب اضافه شده و یکی خواهند شد. خروجی نهایی آن‌ها به شکل زیر در صفحات وب مورد استفاده قرار می‌گیرند:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/css/site.min.css" rel="stylesheet" />
</head>
<body>
    <div>
        <div>
                @RenderBody()
        </div>
    </div>

    <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>
در اینجا تنها دو فایل نهایی این عملیات، یعنی css/site.min.css و js/site.min.js به صفحه الحاق شده‌اند که حاوی تمام پیشنیازهای اسکریپتی و شیوه‌نامه‌های برنامه هستند و در این حالت دیگر نیاز به افزودن آن‌ها به دیگر صفحات سایت نیست.


استفاده از معادل‌های واقعی Unobtrusive Ajax در ASP.NET Core 1.0

واقعیت این است که HTML Helper ای‌جکسی حذف شده‌ی از ASP.NET Core 1.0، کاری بجز افزودن ویژگی‌های data-ajax را که توسط افزونه‌ی jquery.unobtrusive-ajax.min.js پردازش می‌شوند، انجام نمی‌دهد و این افزونه مستقل است از مباحث سمت سرور و به نگارش خاصی از ASP.NET گره نخورده است. بنابراین در اینجا تنها کاری را که باید انجام داد، استفاده از همان ویژگی‌های اصلی است که این افزونه قادر به شناسایی آن‌ها است.
خلاصه‌ی آن‌ها را جهت انتقال کدهای قدیمی و یا تهیه‌ی کدهای جدید، در جدول ذیل می‌توانید مشاهده کنید:

 HTML attribute   AjaxOptions 
 data-ajax-confirm   Confirm 
 data-ajax-method   HttpMethod 
 data-ajax-mode   InsertionMode 
 data-ajax-loading-duration   LoadingElementDuration 
 data-ajax-loading   LoadingElementId 
 data-ajax-begin   OnBegin 
 data-ajax-complete   OnComplete 
 data-ajax-failure   OnFailure 
 data-ajax-success   OnSuccess 
 data-ajax-update   UpdateTargetId 
 data-ajax-url   Url 
   
در ASP.NET Core 1.0، به علت حذف متدهای کمکی Ajax دیگر خبری از AjaxOptions نیست. اما اگر علاقمند به انتقال کدهای قدیمی به ASP.NET Core 1.0 هستید، معادل‌های اصلی این پارامترها را می‌توانید در ستون HTML attribute مشاهده کنید.

چند نکته:
- اگر قصد استفاده‌ی از این ویژگی‌ها را دارید، باید ویژگی "data-ajax="true را نیز حتما قید کنید تا سیستم Unobtrusive Ajax فعال شود.
- ویژگی data-ajax-mode تنها با ذکر data-ajax-update (و یا همان UpdateTargetId پیشین) معنا پیدا می‌کند.
- ویژگی data-ajax-loading-duration نیاز به ذکر data-ajax-loading (و یا همان LoadingElementId پیشین) را دارد.
- ویژگی data-ajax-mode مقادیر before، after و replace-with را می‌پذیرد. اگر قید نشود، کل المان با data دریافتی جایگزین می‌شود.
- سه callback قابل تعریف data-ajax-complete، data-ajax-failure و data-ajax-success، یک چنین پارامترهایی را از سمت سرور در اختیار کلاینت قرار می‌دهند:

parameters  
 Callback  
 xhr, status   data-ajax-complete 
 data, status, xhr   data-ajax-success 
 xhr, status, error   data-ajax-failure 

برای مثال می‌توان ویژگی data-ajax-success را به نحو ذیل در سمت کلاینت مقدار دهی کرد:
 data-ajax-success = "myJsMethod"
این متد جاوا اسکریپتی یک چنین امضایی را دارد:
  function myJsMethod(data, status, xhr) {
}
در این حالت در سمت سرور، پارامتر data در یک اکشن متد، به صورت ذیل مقدار دهی می‌شود:
 return Json(new { param1 = 1, param2 = 2, ... });
و در سمت کلاینت در متد myJsMethod این پارامترها را به صورت data.param1 می‌توان دریافت کرد.


مثال‌هایی از افزودن ویژگی‌های data-ajax به المان‌های مختلف

 در حالت استفاده از Form Tag Helpers که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بررسی شدند، یک فرم ای‌جکسی، چنین تعاریفی را پیدا خواهد کرد:
با این ViewModel فرضی
using System.ComponentModel.DataAnnotations;
 
namespace Core1RtmEmptyTest.ViewModels.Account
{
    public class RegisterViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}
که در View متناظر Ajax ایی ذیل استفاده شده‌است:
@using Core1RtmEmptyTest.ViewModels.Account
@model RegisterViewModel
@{
}
 
<form method="post"
      asp-controller="TestAjax"
      asp-action="Index"
      asp-route-returnurl="@ViewBag.ReturnUrl"
      class="form-horizontal"
      role="form"
      data-ajax="true"
      data-ajax-loading="#Progress"
      data-ajax-success="myJsMethod">
 
    <input asp-for="Email" class="form-control" />
    <span asp-validation-for="Email" class="text-danger"></span>
 
    <button type="submit">ارسال</button>
 
    <div id="Progress" style="display: none">
        <img src="images/loading.gif" alt="loading..." />
    </div>
</form>
 
@section scripts{
    <script type="text/javascript">
        function myJsMethod(data, status, xhr) {
            alert(data.param1);
        }
    </script>
}
در اینجا تمام تعاریف مانند قبل است؛ تنها سه ویژگی data-ajax جهت فعال سازی jquery-ajax-unobtrusive به فرم اضافه شده‌اند. همچنین یک callback دریافت پیام موفقیت آمیز بودن عملیات Ajax ایی نیز تعریف شده‌است.

این View از کنترلر ذیل استفاده می‌کند:
using Core1RtmEmptyTest.ViewModels.Account;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestAjaxController : Controller
    {
 
        public IActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        public IActionResult Index([FromForm]RegisterViewModel vm)
        {
            var ajax = isAjax();
            if (ajax)
            {
                // it's an ajax post
            }
 
 
            if (ModelState.IsValid)
            {
                //todo: save data
 
                return Json(new { param1 = 1, param2 = 2 });
            }
            return View();
        }
 
        private bool isAjax()
        {
            return Request?.Headers != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest";
        }
    }
}
به ASP.NET Core 1.0، متد کمکی IsAjax اضافه نشده‌است؛ اما تعریف آن‌را در این کنترلر مشاهده می‌کنید. در مورد قید FromForm در ادامه توضیح داده خواهد شد (هرچند در این مورد خاص، حالت پیش فرض است و الزامی به قید آن نیست).

و یا Action Link ای‌جکسی نیز به صورت خلاصه به این نحو قابل تعریف است:
<div id="EmployeeInfo">
<a 
 asp-controller="MyController" asp-action="MyAction"
 data-ajax="true" 
 data-ajax-loading="#Progress" 
 data-ajax-method="POST" 
 data-ajax-mode="replace" 
 data-ajax-update="#EmployeeInfo">
 Get Employee-1 info
</a>

  <div id="Progress" style="display: none">
    <img src="images/loading.gif" alt="loading..."  />
  </div>
</div>


نکته‌ای در مورد اکشن متدهای ای‌جکسی در ASP.NET Core 1.0

همانطور که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API»، قسمت «تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API» نیز ذکر شد:
public IActionResult Index([FromBody] MyViewModel vm)
{
   return View();
}
ذکر ویژگی FromBody در اینجا الزامی است. از این جهت که اطلاعات با فرمت JSON، از قسمت body درخواست استخراج و به MyViewModel بایند خواهند شد (در حالت dataType: json). و اگر dataType : application/x-www-form-urlencoded; charset=utf-8 بود (مانند حالت پیش فرض Unobtrusive Ajax)، باید از ویژگی FromForm استفاده شود. در غیر اینصورت در سمت سرور نال دریافت خواهیم کرد.
مطالب
هدایت خودکار کاربر به صفحه لاگین در حین اعمال Ajax ایی
در ASP.NET MVC به کمک فیلتر Authorize می‌توان کاربران را در صورت درخواست دسترسی به کنترلر و یا اکشن متد خاصی در صورت لزوم و عدم اعتبارسنجی کامل، به صفحه لاگین هدایت کرد. این مساله در حین postback کامل به سرور به صورت خودکار رخ داده و کاربر به Login Url ذکر شده در web.config هدایت می‌شود. اما در مورد اعمال Ajax ایی چطور؟ در این حالت خاص، فیلتر Authorize قابلیت هدایت خودکار کاربران را به صفحه لاگین، ندارد. در ادامه نحوه رفع این نقیصه را بررسی خواهیم کرد.

تهیه فیلتر سفارشی SiteAuthorize

برای بررسی اعمال Ajaxایی، نیاز است فیلتر پیش فرض Authorize سفارشی شود:
using System;
using System.Net;
using System.Web.Mvc;

namespace MvcApplication28.Helpers
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public sealed class SiteAuthorizeAttribute : AuthorizeAttribute
    {
        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (filterContext.HttpContext.Request.IsAuthenticated)
            {
                throw new UnauthorizedAccessException(); //to avoid multiple redirects
            }
            else
            {
                handleAjaxRequest(filterContext);
                base.HandleUnauthorizedRequest(filterContext);
            }
        }

        private static void handleAjaxRequest(AuthorizationContext filterContext)
        {
            var ctx = filterContext.HttpContext;
            if (!ctx.Request.IsAjaxRequest())
                return;

            ctx.Response.StatusCode = (int)HttpStatusCode.Forbidden;
            ctx.Response.End();
        }
    }
}
در فیلتر فوق بررسی handleAjaxRequest اضافه شده است. در اینجا درخواست‌های اعتبار سنجی نشده از نوع Ajax ایی خاتمه داده شده و سپس StatusCode ممنوع (403) به کلاینت بازگشت داده می‌شود. در این حالت کلاینت تنها کافی است StatusCode یاده شده را مدیریت کند:
using System.Web.Mvc;
using MvcApplication28.Helpers;

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

        [SiteAuthorize]
        [HttpPost]        
        public ActionResult SaveData(string data)
        {
            if(string.IsNullOrWhiteSpace(data))
                return Content("NOk!");

            return Content("Ok!");
        }
    }
}
در کد فوق نحوه استفاده از فیلتر جدید SiteAuthorize را ملاحظه می‌کنید. View ارسال کننده اطلاعات به اکشن متد SaveData، در ادامه بررسی می‌شود:
@{
    ViewBag.Title = "Index";
    var postUrl = this.Url.Action(actionName: "SaveData", controllerName: "Home");
}
<h2>
    Index</h2>
@using (Html.BeginForm(actionName: "SaveData", controllerName: "Home",
                method: FormMethod.Post, htmlAttributes: new { id = "form1" }))
{
    @Html.TextBox(name: "data")
    <br />
    <span id="btnSave">Save Data</span>
}
@section Scripts
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#btnSave").click(function (event) {
                $.ajax({
                    type: "POST",
                    url: "@postUrl",
                    data: $("#form1").serialize(),
                    // controller is returning a simple text, not json  
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = "/login";
                        }
                    }
                });
            });
        });
    </script>
}
تنها نکته جدید کدهای فوق، بررسی xhr.status == 403 است. اگر فیلتر SiteAuthorize کد وضعیت 403 را بازگشت دهد، به کمک مقدار دهی window.location، مرورگر را وادار خواهیم کرد تا صفحه کنترلر login را نمایش دهد. این کد جاوا اسکریپتی، با تمام مرورگرها سازگار است.


نکته تکمیلی:
در متد handleAjaxRequest، می‌توان یک JavaScriptResult را نیز بازگشت داد تا همان کدهای مرتبط با window.location را به صورت خودکار به صفحه تزریق کند:
filterContext.Result =  new JavaScriptResult { Script="window.location = '" + redirectToUrl + "'"};
البته این روش بسته به نحوه استفاده از jQuery Ajax ممکن است نتایج دلخواهی را حاصل نکند. برای مثال اگر قسمتی از صفحه جاری را پس از دریافت نتایج Ajax ایی از سرور، تغییر می‌دهید، صفحه لاگین در همین قسمت در بین کدهای صفحه درج خواهد شد. اما روش یاد شده در مثال فوق در تمام حالت‌ها کار می‌کند.
مطالب
بهبود صفحه‌‌ی بارگذاری اولیه در Blazor WASM
در بار اول اجرای برنامه‌های Blazor WASM، کار دریافت و کش شدن اسمبلی‌های NET Runtime. و برنامه انجام می‌شوند:


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


افزودن یک progress-bar به صفحه‌ی آغازین برنامه‌های Blazor WASM

Blazor امکان دسترسی به چرخه‌ی حیات ابتدایی آن‌را نیز میسر کرده‌است. برای اینکار ابتدا باید به آن گفت که دریافت خودکار تمام موارد مورد نیاز را انجام نده و ما اینکار را خودمان انجام خواهیم داد:
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
برای اینکار باید به تگ اسکریپتی که به blazor.webassembly.js اشاره می‌کند، ویژگی autostart را با مقدار false، افزود. از این پس باید خودمان کار آغاز Blazor را انجام دهیم که در طی دو مرحله، انجام خواهد شد:
الف) تغییر متن Loading پیش‌فرض جهت نمایش یک progress-bar
<body>
<div id="app">
    <div class="d-flex flex-column min-vh-100">
        <div class="d-flex vh-100">
<div class="d-flex w-100 justify-content-center align-self-center">
<div class="d-flex flex-column w-25">
<div>Loading <label id="progressbarLabel"></label></div>
<div class="progress mt-2" style="height: 2em;">
  <div id="progressbar" class="progress-bar progress-bar-striped"></div>  
</div>
</div>
</div>
        </div>
    </div>
</div>
به فایل index.html برنامه مراجعه کرده و بجای loading پیش‌فرض آن، یک چنین طرحی را قرار می‌دهیم که به همراه یک label و یک progressbar در وسط صفحه است:


ب) سپس فایل جدید js/blazorLoader.js را با محتوای زیر اضافه می‌کنیم. در ابتدای این فایل به المان‌های progressbar و progressbarLabel طرح فوق اشاره می‌شود:
(function () {
  let resourceIndex = 0;
  const fetchResponsePromises = [];
  const progressbar = document.getElementById("progressbar");
  const progressbarLabel = document.getElementById("progressbarLabel");
  const loadStart = new Date().getTime();

  if (!isAutostartDisabled()) {
    console.warn(
      "`blazor.webassembly.js` script tag doesn`t have the `autostart=false` attribute."
    );
    return;
  }

  Blazor.start({
    loadBootResource: function (type, filename, defaultUri, integrity) {
      if (type === "dotnetjs") {
        progressbarLabel.innerText = filename;
        return defaultUri;
      }

      const responsePromise = fetch(defaultUri, {
        cache: "no-cache",
        integrity: integrity,
      });
      fetchResponsePromises.push(responsePromise);
      responsePromise.then((response) => {
        if (!progressbar) {
          console.warn("Couldn't find the progressbar element on the page.");
          return;
        }

        if (!progressbarLabel) {
          console.warn(
            "Couldn't find the progressbarLabel element on the page."
          );
          return;
        }

        resourceIndex++;
        const totalResourceCount = fetchResponsePromises.length;
        const percentLoaded = Math.round(
          100 * (resourceIndex / totalResourceCount)
        );
        progressbar.style.width = `${percentLoaded}%`;
        progressbar.innerText = `${percentLoaded} % [${resourceIndex}/${totalResourceCount}]`;
        progressbarLabel.innerText = filename;
        if (percentLoaded >= 100) {
          var span = new Date().getTime() - loadStart;
          console.log(`All done in ${span} ms.`);
        }
      });

      return responsePromise;
    },
  });

  function isAutostartDisabled() {
    var wasmScript = document.querySelector(
      'script[src="_framework/blazor.webassembly.js"]'
    );
    if (!wasmScript) {
      return false;
    }

    var autostart = wasmScript.attributes["autostart"];
    return autostart && autostart.value === "false";
  }
})();
این فایل باید پس از تعریف مدخل blazor.webassembly.js به فایل index.html اضافه شود:
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script src="js/blazorLoader.js"></script>
</body>
توضیحات:
- محتوای blazorLoader.js، به صورت خود اجرا شونده تهیه شده‌است.
- متد Blazor.start، کار آغاز دستی Blazor WASM و دریافت فایل‌های مورد نیاز آن‌را انجام می‌دهد.
- خاصیت loadBootResource آن، به تابعی اشاره می‌کند که پیشنیازهای اجرایی Blazor WASM را دریافت می‌کند.
- در متد سفارشی loadBootResource که تهیه کرده‌ایم، responsePromise‌ها را شمارش کرده و بر اساس تعداد کلی آن‌ها و مواردی که دریافت آن‌ها به پایان رسیده‌است، یک progress-bar را تشکیل و نمایش می‌دهیم.
- تابع isAutostartDisabled، بررسی می‌کند که آیا ویژگی autostart مساوی false، به تگ اسکریپت blazor.webassembly.js اضافه شده‌است یا خیر؟


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید:  BlazorWasmLoadingBar.zip
نظرات مطالب
ارائه‌ی قالبی عمومی برای استفاده از تقویم‌های جاوااسکریپتی در Blazor

یک نکته‌ی تکمیلی: استفاده از این DatePicker در برنامه‌های Blazor SSR

اگر علاقمند باشید تا از این DatePicker در برنامه‌های Blazor SSR به‌صورت یک کامپوننت استفاده کنید، روش کار به صورت زیر است:

  • نیاز به نسخه‌ی اصلاح شده‌ی آن خواهید داشت.
  • سپس هر المان ورودی که مزین به ویژگی data-dnt-date-picker بود، یافت شده و این DatePicker به آن متصل می‌شود.
  • همچنین از این جهت که در برنامه‌های Blazor SSR، ویژگی enhanced navigation و نمایش Ajax ای صفحات با fetch، فعال است، باید حالت enhancedload را تحت نظر قرار داده و این کوئری گرفتن یافتن عناصر با ویژگی data-dnt-date-picker و اتصال مجددا به آن‌ها را مدیریت کرد.
  • تا همینجا و با این تنظیمات، نمایش این DatePicker فعال می‌شود و ... کار می‌کند. اگر علاقمند به تبدیل آن به یک کامپوننت مخصوص Blazor SSR هم هستید، می‌توانید از کامپوننت DntInputPersianDatePicker.razor و کدهای آن،‌ ایده بگیرید که جزئیات آن پیشتر در مطلب جاری بررسی شده‌است؛ هرچند این مطلب بیشتر به سایر نگارش‌های Blazor می‌پردازد.

مطالب
Blazor 5x - قسمت 19 - کار با فرم‌ها - بخش 7 - نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server
تا قسمت قبل، روشی را که برای کار با EF-Core درنظر گرفتیم، روش متداول کار با آن، در برنامه‌های ASP.NET Core Web API بود؛ یعنی این روش با برنامه‌های مبتنی بر Blazor WASM که از دو قسمت مجزای Web API سمت سرور و Web Assembly سمت کلاینت تشکیل شده‌اند، به خوبی جواب می‌دهد؛ اما ... با Blazor Server یکپارچه که تمام قسمت‌های مدیریتی آن سمت سرور رخ می‌دهند، خیر! در این مطلب، دلایل این موضوع را به همراه ارائه راه‌حلی، بررسی خواهیم کرد.


طول عمر سرویس‌ها، در برنامه‌های Blazor Server متفاوت هستند

هنگامیکه با یک ASP.NET Core Web API متداول کار می‌کنیم، درخواست‌های HTTP رسیده، از میان‌افزارهای موجود رد شده و پردازش می‌شوند. اما هنگامیکه با Blazor Server کار می‌کنیم، به علت وجود یک اتصال دائم SignalR که عموما از نوع Web socket است، دیگر درخواست HTTP وجود ندارد. تمام رفت و برگشت‌های برنامه به سرور و پاسخ‌های دریافتی، از طریق Web socket منتقل می‌شوند و نه درخواست‌ها و پاسخ‌های متداول HTTP.
این روش پردازشی، اولین تاثیری را که بر روی رفتار یک برنامه می‌گذارد، تغییر طول عمر سرویس‌های آن است. برای مثال در برنامه‌های Web API، طول عمر درخواست‌ها، از نوع Scoped هستند و با شروع پردازش یک درخواست، سرویس‌های مورد نیاز وهله سازی شده و در پایان درخواست، رها می‌شوند.
این مساله در حین کار با EF-Core نیز بسیار مهم است؛ از این جهت که در برنامه‌های Web API نیز EF-Core و DbContext آن، به صورت سرویس‌هایی با طول عمر Scoped تعریف می‌شوند. برای مثال زمانیکه یک چنین تعریفی را در برنامه داریم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
امضای واقعی متد AddDbContext مورد استفاده‌ی فوق، به صورت زیر است:
public static IServiceCollection AddDbContext<TContext>(
    [NotNullAttribute] this IServiceCollection serviceCollection, 
    [CanBeNullAttribute] Action<DbContextOptionsBuilder> optionsAction = null, 
    ServiceLifetime contextLifetime = ServiceLifetime.Scoped, 
    ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext;
همانطور که مشاهده می‌کنید، طول عمرهای پیش‌فرض تعریف شده‌ی در اینجا، از نوع Scoped هستند. یعنی زمانیکه سرویس‌های ApplicationDbContext را از طریق سیستم تزریق وابستگی‌های برنامه دریافت می‌کنیم، در ابتدای یک درخواست Web API، به صورت خودکار وهله سازی شده و در پایان درخواست رها می‌شوند. به این ترتیب به ازای هر درخواست رسیده، وهله‌ی متفاوتی از DbContex را دریافت می‌کنیم که با وهله‌ی استفاده شده‌ی در درخواست قبلی، یکی نیست.
اما زمانیکه مانند یک برنامه‌ی مبتنی بر Blazor Server، دیگر HTTP Requests متداولی را نداریم، چطور؟ در این حالت زمانیکه یک اتصال SignalR برقرار شد، وهله‌ای از DbContext که در اختیار برنامه‌ی Blazor Server قرار می‌گیرد، تا زمانیکه کاربر این اتصال را به نحوی قطع نکرده (مانند بستن کامل مرورگر و یا ریفرش صفحه)، ثابت باقی خواهد ماند. یعنی به ازای هر اتصال SignalR، طول عمر ServiceLifetime.Scoped پیش‌فرض تعریف شده، همانند یک وهله‌ی با طول عمر Singleton عمل می‌کند. در این حالت تمام صفحات و کامپوننت‌های یک برنامه‌ی Blazor Server، از یک تک وهله‌ی مشخص DbContext که در ابتدای کار دریافت کرده‌اند، کار می‌کنند و از آنجائیکه DbContext به صورت thread-safe کار نمی‌کند، این تک وهله مشکلات زیادی را ایجاد خواهد کرد که یک نمونه از آن‌را در عمل، در پایان قسمت قبل مشاهده کردید:
«اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر می‌رسیم و یا حتی اگر کاربر شروع کند به کلیک کردن سریع در قسمت‌های مختلف برنامه، باز هم این خطا مشاهده می‌شود:
 An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed.
This is usually caused by different threads concurrently using the same instance of DbContext.
For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
عنوان می‌کند که متد OnConfirmDeleteRoomClicked، بر روی ترد دیگری نسبت به ترد اولیه‌ای که DbContext بر روی آن ایجاد شده، در حال اجرا است و چون DbContext برای یک چنین سناریوهایی، thread-safe نیست، اجازه‌ی استفاده‌ی از آن‌را نمی‌دهد.»
هر درخواست Web API نیز بر روی یک ترد جداگانه اجرا می‌شود؛ اما چون ابتدا و انتهای درخواست‌ها مشخص است، طول عمر Scoped، در ابتدای درخواست شروع شده و در پایان آن رها سازی می‌شود. به همین جهت استثنائی را که در اینجا مشاهده می‌کنید، در برنامه‌های Web API شاید هیچگاه مشاهده نشود.


معرفی DbContextFactory در EF Core 5x

همواره باید طول عمر DbContext را تا جای ممکن، کوتاه نگه داشت. مشکل فعلی ما، Singleton رفتار کردن DbContext‌ها (داشتن طول عمر طولانی) در برنامه‌های Blazor Server هستند. یک چنین رفتاری را شاید در برنامه‌های دسکتاپ هم پیشتر مشاهده کرده باشید. برای مثال در برنامه‌های دسکتاپ WPF، تا زمانیکه یک فرم باز است، Context ایجاد شده‌ی در آن هم برقرار است و Dispose نمی‌شود. در یک چنین حالت‌هایی، عموما Context را در زمان نیاز، ایجاد کرده و پس از پایان آن کار کوتاه، Context را رها می‌کنند. به همین جهت نیاز به DbContext Factory ای وجود دارد که بتواند یک چنین پیاده سازی‌هایی را میسر کند و خوشبختانه از زمان EF Core 5x، یک چنین امکانی خصوصا برای برنامه‌های Blazor Server تحت عنوان DbContextFactory ارائه شده‌است که به عنوان راه حل استاندارد دسترسی به DbContext در اینگونه برنامه‌ها مورد استفاده قرار می‌گیرد.
برای کار با DbContextFactory، اینبار در فایل BlazorServer.App\Startup.cs، بجای استفاده از services.AddDbContext، از متد AddDbContextFactory استفاده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    var connectionString = Configuration.GetConnectionString("DefaultConnection");
    //services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
    services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
سپس باید دقت داشت که روش استفاده‌ی از آن، نسبت به کار مستقیم با ApplicationDbContext، کاملا متفاوت است. هدف از DbContextFactory، ساخت دستی Context در زمان نیاز و سپس Dispose صریح آن است. بنابراین طول عمر Context دریافت شده‌ی توسط آن باید توسط برنامه نویس مدیریت شود و به صورت خودکار توسط IoC Container برنامه مدیریت نخواهد شد. در این حالت دو روش استفاده‌ی از آن در کامپوننت‌های برنامه‌های Blazor Server، پیشنهاد می‌شود.


روش اول کار با DbContextFactory در کامپوننت‌های Blazor Server : وهله سازی از نو، به ازای هر متد

در این روش پس از ثبت AddDbContextFactory در فایل Startup برنامه مانند مثال فوق، ابتدا سرویس IDbContextFactory که به ApplicationDbContext اشاره می‌کند به ابتدای کامپوننت تزریق می‌شود:
@inject IDbContextFactory<ApplicationDbContext> DbFactory
سپس در هر جائی که نیاز به وهله‌ای از ApplicationDbContext است، آن‌را به صورت دستی وهله سازی کرده و همانجا هم Dispose می‌کنیم:
private async Task DeleteImageAsync()
{
    using var context = DbFactory.CreateDbContext();

    var image = await context.HotelRoomImages.FindAsync(1);

   // ...
}
در اینجا یکی متدهای یک کامپوننت فرضی را مشاهده می‌کند که از DbFactory تزریق شده استفاده کرد و سپس با استفاده از متد ()CreateDbContext، وهله‌ی جدیدی از ApplicationDbContext را ایجاد می‌کند. همچنین در همان سطر، وجود عبارت using نیز مشاهده می‌شود. یعنی در پایان کار این متد، context ایجاد شده حتما Dispose شده و طول عمر کوتاهی خواهد داشت.


روش دوم کار با DbContextFactory در کامپوننت‌های Blazor Server : یکبار وهله سازی Context به ازای هر کامپوننت

در این روش می‌توان طول عمر Context را معادل طول عمر کامپوننت تعریف کرد که مزیت استفاده‌ی از Change tracking موجود در EF-Core را به همراه خواهد داشت. در این حالت کامپوننت‌های Blazor Server، شبیه به فرم‌های برنامه‌های دسکتاپ عمل می‌کنند:
@implements IDisposable
@inject IDbContextFactory<ApplicationDbContext> DbFactory


@code
{
   private ApplicationDbContext Context;

   protected override async Task OnInitializedAsync()
   {
       Context = DbFactory.CreateDbContext();
       await base.OnInitializedAsync();
   }

   private async Task DeleteImageAsync()
   {
       var image = await Context.HotelRoomImages.FindAsync(1);
       // ...
   }

   public void Dispose()
   {
     Context.Dispose();
   }
}
- در اینجا همانند روش اول، کار با تزریق IDbContextFactory شروع می‌شود
-  اما بجای اینکه به ازای هر متد، کار فراخوانی DbFactory.CreateDbContext صورت گیرد، یکبار در آغاز کار کامپوننت و در روال رویدادگردان OnInitializedAsync، کار وهله سازی Context کامپوننت انجام شده و از این تک Context در تمام متدهای کامپوننت استفاده خواهد شد.
- در این حالت کار Dispose خودکار این Context به متد Dispose نهایی کل کامپوننت واگذار شده‌است. برای اینکه این متد فراخوانی شود، نیاز است در ابتدای تعاریف کامپوننت، از دایرکتیو implements IDisposable@ استفاده کرد.


سؤال: اگر سرویسی از ApplicationDbContext تزریق شده‌ی در سازنده‌ی خود استفاده می‌کند، چکار باید کرد؟

برای نمونه سرویس‌های از پیش تعریف شده‌ی ASP.NET Core Identity، در سازنده‌ی خود از ApplicationDbContext استفاده می‌کنند و نه از IDbContextFactory. در این حالت برای تامین ApplicationDbContext‌های تزریق شده، فقط کافی است از روش زیر استفاده کنیم:
services.AddScoped<ApplicationDbContext>(serviceProvider =>
     serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
در این حالت به ازای هر Scope تعریف شده‌ی در برنامه، جهت دسترسی به ApplicationDbContext از طریق سیستم تزریق وابستگی‌ها، کار فراخوانی DbFactory.CreateDbContext به صورت خودکار انجام خواهد شد.


سؤال: روش پیاده سازی سرویس‌های یک برنامه Blazor Server به چه صورتی باید تغییر کند؟

تا اینجا روش‌هایی که برای استفاده از IDbContextFactory معرفی شدند (که روش‌های رسمی و توصیه شده‌ی اینکار نیز هستند)، فرض را بر این گذاشته‌اند که ما قرار است تمام منطق تجاری کار با بانک اطلاعاتی را داخل همان متدهای کامپوننت‌ها انجام دهیم (این روش برنامه نویسی، بسیار مورد علاقه‌ی مایکروسافت است و در تمام مثال‌های رسمی آن به صورت ضمنی توصیه می‌شود!). اما اگر همانند مثالی که تاکنون در این سری بررسی کردیم، نخواهیم اینکار را انجام دهیم و علاقمند باشیم تا این منطق تجاری را به سرویس‌های مجزایی، با مسئولیت‌های مشخصی انتقال دهیم، روش استفاده‌ی از IDbContextFactory چگونه خواهد بود؟
در این حالت از ترکیب روش دوم مطرح شده‌ی استفاده از IDbContextFactory که به همراه مزیت دسترسی کامل به Change Tracking توکار EF-Core و پیاده سازی الگوی واحد کار است و وهله سازی خودکار ApplicationDbContext که معرفی شد، استفاده خواهیم کرد؛ به این صورت:
الف) تمام سرویس‌های EF-Core یک برنامه‌ی Blazor Server باید اینترفیس IDisposable را پیاده سازی کنند.
این مورد برای سرویس‌های پروژه‌های Web API، ضروری نیست؛ چون طول عمر Context آن‌ها توسط خود IoC Container مدیریت می‌شود؛ اما در برنامه‌های Blazor Server، مطابق توضیحاتی که ارائه شد، خودمان باید این طول عمر را مدیریت کنیم.
بنابراین به پروژه‌ی سرویس‌های برنامه مراجعه کرده و هر سرویسی که ApplicationDbContext تزریق شده‌ای را در سازنده‌ی خود می‌پذیرد، یافته و تعریف اینترفیس آن‌را به صورت زیر تغییر می‌دهیم:
public interface IHotelRoomService : IDisposable
{
   // ...
}

public interface IHotelRoomImageService : IDisposable
{
   // ...
}
سپس باید اینترفیس‌های IDisposable را پیاده سازی کرد که روش مورد پذیرش code analyzer‌ها در این زمینه، رعایت الگوی زیر، دقیقا به همین شکل است و باید از دو متد تشکیل شود:
    public class HotelRoomService : IHotelRoomService
    {
        private bool _isDisposed;

        // ...

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        _dbContext.Dispose();
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }
این الگو را به همین شکل برای سرویس HotelRoomImageService نیز پیاده سازی می‌کنیم.


ب) Dispose دستی تمام سرویس‌ها، در کامپوننت‌های مرتبط
در ادامه تمام کامپوننت‌هایی را که از سرویس‌های فوق استفاده می‌کنند یافته و ابتدا دایرکتیو implements IDisposable@ را به ابتدای آن‌ها اضافه می‌کنیم. سپس متد Dispose آن‌ها را جهت فراخوانی متد Dispose سرویس‌های فوق، تکمیل خواهیم کرد:
بنابراین ابتدا به فایل BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomUpsert.razor مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
@page "/hotel-room/create"
@page "/hotel-room/edit/{Id:int}"

@implements IDisposable
// ...


@code
{
    // ...

    public void Dispose()
    {
        HotelRoomImageService.Dispose();
        HotelRoomService.Dispose();
    }
}
و همچنین به کامپوننت BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و آن‌را به صورت زیر جهت Dispose دستی سرویس‌ها، تکمیل می‌کنیم:
@page "/hotel-room"

@implements IDisposable
// ...


@code
{
    // ...

    public void Dispose()
    {
        HotelRoomService.Dispose();
    }
}


مشکل! اینبار خطای dispose شدن context را دریافت می‌کنیم!

System.ObjectDisposedException: Cannot access a disposed context instance.
A common cause of this error is disposing a context instance that was resolved from dependency injection and then
later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose'
on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the
dependency injection container take care of disposing context instances.
Object name: 'ApplicationDbContext'.
هم برنامه‌های Blazor WASM و هم برنامه‌های Blazor Server از مفهوم طول عمرهای تنظیم شده‌ی سرویس‌ها پشتیبانی نمی‌کنند! در هر دوی این‌ها اگر سرویسی را با طول عمر Scoped تنظیم کردیم، رفتار آن همانند سرویس‌های Singleton خواهد بود. تنها زمانی رفتارهای Scoped و یا Transient پشتیبانی می‌شوند که درخواست HTTP ای رخ داده باشد که این مورد خارج است از طول عمر یک برنامه‌ی Blazor WASM و همچنین اتصال SignalR برنامه‌های Blazor Server. فقط قسمت‌هایی از برنامه‌ی Blazor Server که با مدل قبلی Razor pages طراحی شده‌اند، چون سبب شروع یک درخواست HTTP معمولی می‌شوند، همانند برنامه‌های متداول ASP.NET Core رفتار می‌کنند و در این حالت طول عمرهای غیر Singleton مفهوم پیدا می‌کنند.

مشکلی که در اینجا رخ داده این است که سرویس‌هایی را داریم با طول عمر به ظاهر Scoped که یکی از وابستگی‌های آن‌ها را به صورت دستی Dispose کرده‌ایم. چون طول عمر Scoped در اینجا وجود ندارد و طول عمرها در اصل Singleton هستند، هربار که سرویس مدنظر مجددا درخواست شود، همان وهله‌ی ابتدایی که اکنون یکی از وابستگی‌های آن Dispose شده، در اختیار برنامه قرار می‌گیرد.
پس از این تغییرات، اولین باری که برنامه را اجرا می‌کنیم، لیست اتاق‌ها به خوبی نمایش داده می‌شوند و مشکلی نیست. بعد در همین حال و در همین صفحه، اگر بر روی دکمه‌ی افزودن یک اتاق جدید کلیک کنیم، اتفاقی که رخ می‌دهد، فراخوانی متد Dispose کامپوننت لیست اتاق‌ها است (بر روی آن یک break-point قرار دهید). بنابراین متد Dispose یک کامپوننت، با هدایت به یک مسیر دیگر، به صورت خودکار فراخوانی می‌شود. در این حالت Context برنامه Dispose شده و در کامپوننت ثبت یک اتاق جدید دیگر، در دسترس نخواهد بود؛ چون IHotelRoomService مورد استفاده مجددا وهله سازی نمی‌شود و از همان وهله‌ای که بار اول ایجاد شده، استفاده خواهد شد.
 
بنابراین سؤال اینجا است که چگونه می‌توان سیستم تزریق وابستگی‌ها را وادار کرد تا تمام سرویس‌های تزریق شده‌ی به سازنده‌ها‌ی سرویس‌های HotelRoomService و  HotelRoomImageService را مجددا وهله سازی کند و سعی نکند از همان وهله‌های قبلی استفاده کند؟

پاسخ: یک روش این است که IHotelRoomImageService را خودمان به ازای هر کامپوننت به صورت دستی در روال رویدادگردان OnInitializedAsync وهله سازی کرده و DbFactory.CreateDbContext جدیدی را مستقیما به سازنده‌ی آن ارسال کنیم. در این حالت مطمئن خواهیم شد که این وهله، جای دیگری به اشتراک گذاشته نمی‌شود:
@code
{
   private IHotelRoomImageService HotelRoomImageService;

   protected override async Task OnInitializedAsync()
   {
       HotelRoomImageService =  new HotelRoomImageService(DbFactory.CreateDbContext(), mapper);
       await base.OnInitializedAsync();
   }

   private async Task DeleteImageAsync()
   {
       await HotelRoomImageService.DeleteAsync(1);
       // ...
   }

   public void Dispose()
   {
     HotelRoomImageService.Dispose();
   }
}
هرچند این روش کار می‌کند، اما در زمان استفاده از IoC Container‌ها قرار نیست کار انجام new‌ها را خودمان به صورت دستی انجام دهیم و بهتر است مدیریت این مساله به آن‌ها واگذار شود.


وادار کردن Blazor Server به وهله سازی مجدد سرویس‌های کامپوننت‌ها

بنابراین مشکل ما Singleton رفتار کردن سرویس‌ها، در برنامه‌های Blazor است. برای مثال در برنامه‌های Blazor Server، تا زمانیکه اتصال SignalR برنامه برقرار است (مرورگر بسته نشده، برگه‌ی جاری بسته نشده و یا کاربر صفحه را ریفرش نکرده)، هیچ سرویسی دوباره وهله سازی نمی‌شود.
برای رفع این مشکل، امکان Scoped رفتار کردن سرویس‌های یک کامپوننت نیز در نظر گرفته شده‌اند. برای نمونه کدهای کامپوننت HotelRoomList.razor را به صورت زیر تغییر می‌دهیم:
@page "/hotel-room"

@*@implements IDisposable*@
@*@inject IHotelRoomService HotelRoomService*@
@inherits OwningComponentBase<IHotelRoomService>
با استفاده از دایرکتیو جدید inherits OwningComponentBase@ می‌توان میدان دید یک سرویس را به طول عمر کامپوننت جاری محدود کرد. هربار که این کامپوننت نمایش داده می‌شود، وهله سازی شده و هربار که به کامپوننت دیگری هدایت می‌شویم، به صورت خودکار سرویس مورد استفاده را Dispose می‌کند. بنابراین در اینجا دیگر نیازی به ذکر دایرکتیو implements IDisposable@ نیست.

چند نکته:
- فقط یکبار به ازای هر کامپوننت می‌توان از دایرکتیو inherits استفاده کرد.
- زمانیکه طول عمر سرویسی را توسط OwningComponentBase مدیریت می‌کنیم، در حقیقت یک کلاس پایه را برای آن کامپوننت درنظر گرفته‌ایم که به همراه یک خاصیت عمومی ویژه، به نام Service و از نوع سرویس مدنظر ما است. در این حالت یا می‌توان از خاصیت Service به صورت مستقیم استفاده کرد و یا می‌توان به صورت زیر، همان کدهای قبلی را داشت و هربار که نیازی به HotelRoomService بود، آن‌را به خاصیت عمومی Service هدایت کرد:
@code
{
   private IHotelRoomService HotelRoomService => Service;
- اگر نیاز به بیش از یک سرویس وجود داشت، چون نمی‌توان بیش از یک inherits را تعریف کرد، می‌توان از نمونه‌ی غیرجنریک OwningComponentBase استفاده کرد:
@page "/preferences"
@using Microsoft.Extensions.DependencyInjection
@inherits OwningComponentBase


@code {
    private IHotelRoomService HotelRoomService { get; set; }
    private IHotelRoomImageService HotelRoomImageService { get; set; }

    protected override void OnInitialized()
    {
        HotelRoomService = ScopedServices.GetRequiredService<IHotelRoomService>();
        HotelRoomImageService = ScopedServices.GetRequiredService<IHotelRoomImageService>();
    }
}
در این حالت کلاس پایه‌ی OwningComponentBase، به همراه خاصیت جدید ScopedServices است که با فراخوانی متد GetRequiredService در روال رویدادگردان OnInitialized بر روی آن، سبب وهله سازی Scoped سرویس مدنظر خواهد شد. نمونه‌ی جنریک آن، تمام این موارد را در پشت صحنه انجام می‌دهد و کار کردن با آن ساده‌تر و خلاصه‌تر است.


خلاصه‌ی بحث جاری در مورد روش مدیریت DbContext برنامه‌های Blazor Server:

- بجای services.AddDbContext متداول، باید از AddDbContextFactory استفاده کرد:
services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
services.AddScoped<ApplicationDbContext>(serviceProvider =>
        serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
- تمام سرویس‌هایی که از ApplicationDbContext استفاده می‌کنند، باید به همراه پیاده سازی Dispose آن نیز باشند؛ چون Scope یک سرویس، معادل طول عمر اتصال SignalR برنامه است و مدام وهله سازی نمی‌شود. در این حالت باید وهله سازی و Dispose آن‌را دستی مدیریت کرد.
- کامپوننت‌های برنامه، سرویس‌هایی را که باید Scoped عمل کنند، دیگر نباید از طریق تزریق مستقیم آن‌ها دریافت کنند؛ چون در این حالت همواره به همان وهله‌ای که در ابتدای کار ایجاد شده، می‌رسیم:
@inject IHotelRoomService HotelRoomService
این دریافت باید با استفاده از کلاس پایه OwningComponentBase صورت گیرد:
@inherits OwningComponentBase<IHotelRoomService>
تا عملیات فراخوانی خودکار ScopedServices.GetRequiredService (دریافت وهله‌ی جدید Scoped) و همچنین Dispose خودکار آن‌ها را به ازای هر کامپوننت مجزا، مدیریت کند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-19.zip
اشتراک‌ها
تغییرات ASP.NET Core در NET 6 Preview 3.

Here’s what’s new in this preview release:

  • Smaller SignalR, Blazor Server, and MessagePack scripts
  • Enable Redis profiling sessions
  • HTTP/3 endpoint TLS configuration
  • Initial .NET Hot Reload support
  • Razor compiler no longer produces a separate Views assembly
  • Shadow-copying in IIS
  • Vcpkg port for SignalR C++ client
  • Reduced memory footprint for idle TLS connections
  • Remove slabs from the SlabMemoryPool
  • BlazorWebView controls for WPF & Windows Forms 
تغییرات ASP.NET Core در NET 6 Preview 3.
مطالب
LocalDB FAQ
SQL Server Express LocalDB یا به صورت خلاصه LocalDB، یک بانک اطلاعاتی‌است که به صورت متصل به پروسه‌ی برنامه‌ی جاری اجرا می‌شود؛ برخلاف رویه‌ی متداول بانک‌های اطلاعاتی که به صورت یک سرویس مستقل اجرا می‌شوند. هدف آن، جایگزین کردن نگارش Express نیست و بیشتر حجم کم و سهولت توزیع آن مدنظر بوده‌است. برای مثال نگارش Express به صورت یک سرویس مجزا و مستقل بر روی سیستم نصب می‌شود؛ اما LocalDB به همراه و متصل به برنامه‌ی نوشته شده، اجرا می‌شود:


اگر به تصویر فوق دقت کنید، یک child process جدید به نام sqlservr.exe نیز به همراه برنامه‌ی آزمایشی ما به صورت خودکار اجرا شده‌است. این child process به همراه پارامترهای ذیل است (که توسط NET Framework. مقدار دهی می‌شوند و مدیریت نهایی آن خودکار است):
 "C:\Program Files\Microsoft SQL Server\120\LocalDB\Binn\\sqlservr.exe"   
-c -SMSSQL12E.LOCALDB
-sLOCALDB#5657074F
-d"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\master.mdf"
-l"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\mastlog.ldf"
-e"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\error.log"
بنابراین LocalDB برخلاف SQL Server CE، یک بانک اطلاعاتی in-process نیست و به صورت یک پروسه‌ی مجزا اجرا می‌شود. زمانیکه از SQL Server CE استفاده می‌شود، موتور این بانک اطلاعاتی چند فایل DLL بیشتر نیستند و نهایتا اجرای آن داخل پروسه‌ی برنامه‌ی ما و همانند اجرای سایر DLLهای متصل و مورد استفاده‌ی به آن است.
اما LocalDB یک بانک اطلاعاتی user-mode است و در پروفایل کاربر جاری سیستم اجرا می‌شود. این بانک اطلاعاتی یک بار بر روی سیستم نصب می‌شود و در هر برنامه‌ای که از آن استفاده می‌کنید، یک child process مجزای خاص خودش را (sqlservr.exe) اجرا خواهد کرد. اجرا و خاتمه‌ی این child processها نیز خودکار هستند و نیازی به دخالت مستقیم برنامه ندارند.
البته به نظر توسعه‌ی SQL Server CE متوقف شده‌است و دیگر پشتیبانی نمی‌شود. بنابراین گزینه‌ی ترجیح داده شده‌ی برای کارهایی با حجم‌های بانک اطلاعاتی زیر 10 گیگابایت ، می‌تواند LocalDB باشد. به علاوه اینکه قابلیت‌های T-SQL بیشتری را نیز پشتیبانی می‌کند و همچنین پشتیبانی منظمی نیز از آن وجود دارد. برای مثال پیش نمایش نگارش 2016 آن نیز موجود است.

در ادامه، یک سری پرسش و پاسخ متداول جهت کار با LocalDB را مرور خواهیم کرد.


محل دریافت آخرین نگارش مستقل آن کجاست؟

همانطور که عنوان شد، یکی از مهم‌ترین اهداف LocalDB، سهولت توزیع آن است و عدم نیاز به یک Admin سیستم، برای نصب و نگهداری آن. نگارش 2014 SP1 آن‌را از آدرس ذیل می‌توانید دریافت کنید:
https://www.microsoft.com/en-us/download/details.aspx?id=46697

در اینجا نسخه‌های متعددی وجود دارند. برای مثال اگر سیستم شما 64 بیتی است، تنها نیاز است ENU\x64\SqlLocalDB.msi را دریافت و نصب کنید:



پارامترهای نصب خاموش آن برای توزیع ساده‌ی برنامه کدامند؟

اگر می‌خواهید نصاب LocalDB را به همراه setup برنامه‌ی خود توزیع کنید، می‌توانید روش توزیع خاموش را با ذکر پارامترهای ذیل، مورد استفاده قرار دهید:
 msiexec /i SqlLocalDB.msi /qn IACCEPTSQLLOCALDBLICENSETERMS=YES


رشته‌ی اتصالی مخصوص آن کدام است؟

  <connectionStrings>
    <add name="Sample35Context"
        connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\test.mdf;Integrated Security=True;"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
اگر نگارش 2014 SP1 آن‌را نصب کرده باشید، رشته‌ی اتصالی فوق، تمام آن‌چیزی است که برای شروع به کار با آن، نیاز دارید و دارای دو قسمت مهم است:
الف) ذکر وهله‌ی مدنظر
در اینجا وهله‌ی MSSQLLocalDB ذکر شده‌است؛ اما چه وهله‌هایی بر روی سیستم نصب هستند و چطور می‌توان وهله‌ی دیگری را ایجاد کرد؟ برای این منظور، به پارامترهای sqlservr.exe ابتدای بحث دقت کنید. اکثر آن‌ها به پوشه‌ی ذیل اشاره می‌کنند:
 C:\Users\your_user_name_here\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
با یک چنین محتوایی

در این پوشه، وهله‌‌های موجود و نصب شده‌ی بر روی سیستم شما نمایش داده می‌شوند که یکی از آن‌ها را می‌توانید در رشته‌ی اتصالی فوق ذکر کنید.
به علاوه، این لیست را توسط برنامه‌ی کمکی SqlLocalDB.exe، به همراه پارامتر info یا i نیز می‌توانید دریافت و بررسی کنید:


برنامه‌ی کمکی SqlLocalDB.exe به همراه نصاب LocalDB، نصب می‌شود و توسط آن می‌توان نگارش‌های مختلف نصب شده‌را با پارامتر v و وهله‌ی مختلف موجود را با پارامتر i مشاهده کرد.
همچنین اگر می‌خواهید وهله‌ی جدیدی را بجز وهله‌ی پیش فرض MSSQLLocalDB ایجاد کنید، می‌توانید از پارامتر create آن به نحو ذیل استفاده نمائید:
For LocalDB SQL EXPRESS 2014
 "C:\Program Files\Microsoft SQL Server\120\Tools\Binn\SqlLocalDB.exe" create "v12.0" 12.0 -s

For LocalDB SQL Express 2012
 "C:\Program Files\Microsoft SQL Server\110\Tools\Binn\SqlLocalDB.exe" create "v11.0" 11.0 -s

ب) ذکر DataDirectory
در رشته‌ی اتصالی فوق، پارامتر DataDirectory نیز ذکر شده‌است تا بتوان مسیر بانک اطلاعاتی را به صورت نسبی و بدون ذکر عبارت دقیق آن که ممکن است در سیستم‌های دیگر متفاوت باشد، پردازش کرد. این پارامتر در برنامه‌های وب به پوشه‌ی استاندارد app_data اشاره می‌کند و نیازی به تنظیم اضافه‌تری ندارد. اما در برنامه‌های دسکتاپ باید به نحو ذیل به صورت دستی، در آغاز برنامه مقدار دهی شود:
 AppDomain.CurrentDomain.SetData("DataDirectory", AppDomain.CurrentDomain.BaseDirectory);
به این ترتیب DataDirectory به محل قرارگیری فایل exe برنامه اشاره می‌کند. بدیهی است در اینجا هر پوشه‌ی دیگری را نیز می‌توانید ذکر کنید:
 AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "db"));
برای نمونه تنظیم فوق به زیر پوشه‌ی db، در کنار فایل exe برنامه اشاره می‌کند.


محل نصب بانک‌های اطلاعاتی پیش فرض آن کدام است؟

ذکر AttachDbFilename در رشته‌ی اتصالی فوق، اختیاری است. در صورت عدم ذکر آن، بانک اطلاعاتی ایجاد شده را در یکی از مسیرهای ذیل می‌توانید جستجو کنید:
 C:\Users\USERNAME\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
C:\Users\USERNAME\AppData\Local\Microsoft\VisualStudio\SSDT
همچنین در این محل‌ها فایل‌های log متنی خطاهای این بانک اطلاعاتی را نیز می‌توان مشاهده کرد. بنابراین اگر به خطای خاصی برخوردید، بهترین کار، بررسی این فایل‌‌ها است.


آیا می‌توان فایل‌های mdf و ldf آن‌را به نگارش کامل SQL Server متصل (attach) کرد؟

بله. اما باید دقت داشته باشید که SQL Server به محض اتصال یک بانک اطلاعاتی با نگارش پایین‌تر به آن، ابتدا شماره نگارش آن‌‌را به روز می‌کند. یعنی دیگر نخواهید توانست این بانک اطلاعاتی را با نگارش پایین‌تر LocalDB باز کنید و یک چنین پیام خطایی را دریافت خواهید کرد:
 The database xyz cannot be opened because it is version 706. This server supports version 663 and earlier. A downgrade path is not supported.


چگونه محتوای بانک‌های اطلاعاتی LocalDB را با VS.NET مشاهده کنیم؟

از منوی view گزینه‌ی server explorer را انتخاب کنید. بر روی data connections کلیک راست کرده و گزینه‌ی Add connection را انتخاب کنید.


در صفحه‌ی باز شده، گزینه‌ی Microsoft SQL server  را انتخاب کنید. در صفحه‌ی بعد، ذکر server name مطابق data source رشته‌ی اتصالی بحث شده و سپس انتخاب گزینه‌ی attach a database file کفایت می‌کند:


پس از کلیک بر روی ok، امکان کار با اجزای این بانک اطلاعاتی را خواهید داشت:



چگونه از LocalDB با EF استفاده کنیم؟

EF 6.x به صورت پیش فرض از بانک اطلاعاتی LocalDB استفاده می‌کند و تنها داشتن یک چنین تنظیمی در فایل کانفیگ برنامه، برای کار با آن کافی است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="Sample35Context"
        connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\test.mdf;Integrated Security=True;"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
</configuration>
یک قسمت آن ذکر رشته‌ی اتصالی است که در مورد آن بحث شد و قسمت دوم آن، ذکر connection factory مخصوص localdb است که به صورت فوق می‌باشد. تنظیم دیگری برای کار با LocalDB و EF 6.x نیازی نیست.
البته باید دقت داشت که اسمبلی EntityFramework.SqlServer نیز به صورت خودکار به همراه بسته‌ی نیوگت EF 6.x به برنامه اضافه می‌شود که استفاده‌ی از connection factory ذکر شده را میسر می‌کند.


استفاده‌ی از LocalDB به همراه برنامه‌های وب چگونه است؟

سه نکته را باید در حین استفاده‌ی از LocalDB، در برنامه‌های وب اجرا شده‌ی بر روی IIS مدنظر داشت:
الف) LocalDB یک بانک اطلاعاتی user-mode است و child process آن تحت مجوز اکانت تنظیم شده‌ی برای آن کار می‌کند.
ب) همانطور که عنوان شد، در رشته‌ی اتصالی ذکر شده، پارامتر DataDirectory به پوشه‌ی استاندارد app_data اشاره می‌کند که فایل‌های قرار گرفته‌ی در آن توسط IIS محافظت می‌شوند و از طریق وب قابل دسترسی و دانلود نیستند.
ج) child process مربوط به LocalDB، نیاز به دسترسی write، برای کار با فایل‌های mdf و ldf خود دارد.

برای مورد الف نیاز است تا به تنظیمات application pool برنامه مراجعه کرده و سپس بر روی آن کلیک راست کرد و گزینه‌ی advanced settings را انتخاب نمود. در اینجا گزینه‌ی load user profile باید true باشد:


تنظیم load user profile ضروری است اما کافی نیست. پس از آن باید setProfileEnvironment را نیز به true تنظیم کرد. تنظیم این مورد در کنسول مدیریتی IIS به صورت زیر است.
ابتدا ریشه‌ی اصلی سرور را انتخاب کنید و سپس به configuration editor آن وارد شوید:


در ادامه از دارپ داون آن، گزینه‌ی system.applicationHost و زیر شاخه‌ی applicationPools آن‌را انتخاب کنید:


در اینجا application pool defaults و سپس در آن processModel را نیز باز کنید:


اکنون امکان ویرایش setProfileEnvironment را به true خواهید داشت:


پس از این تنظیم، ابتدا بر روی دکمه‌ی apply سمت راست صفحه کلیک کرده و سپس نیاز است یکبار IIS را نیز ریست کنید تا تنظیمات اعمال شوند.


در ادامه برای تنظیم دسترسی write (موارد ب و ج)، ابتدا بر روی پوشه‌ی app_data برنامه، کلیک راست کرده و برگه‌ی security آن‌را باز کنید. سپس بر روی دکمه‌ی edit کلیک کرده و در صفحه‌ی باز شده بر روی دکمه‌ی add کلیک کنید تا بتوان به کاربر application pool برنامه دسترسی write داد:


در اینجا iis apppool\TestLocalDB را وارد کرده و بر روی دکمه‌ی check name کلیک کنید.

iis apppool آن که مشخص است. عبارت TestLocalDB نام application pool ایی است که برای برنامه‌ی وب خود ایجاد کرده‌ایم (بهتر است به ازای هر برنامه‌ی وب، یک application pool مجزا تعریف شود).


در اینجا بر روی OK کلیک کرده و به این کاربر جدید اضافه شده، دسترسی full control را بدهید تا برنامه و یوزر آن بتواند فایل‌های mdf و ldf را ایجاد کرده و به روز رسانی کنند.

پس از تنظیم load user profile و همچنین set profile environment و دادن دسترسی write به کاربر application pool برنامه، اکنون child process مربوط به local db را می‌توان ذیل پروسه‌ی IIS مشاهده کرد و برنامه قادر به استفاده‌ی از LocalDB خواهد بود: