نظرات مطالب
هدایت درخواست فایل‌های استاتیک در ASP.NET MVC به یک کنترلر
- دیالوگ لاگین در IDM برای حالت Basic authentication ظاهر می‌شود. 
+ نرم افزار IDM اگر از طریق افزونه‌های آن (^) اقدام به دریافت این فایل کند، مشکلی نخواهد داشت؛ چون این افزونه‌ها اطلاعات سشن جاری کاربر را به برنامه، جهت شبیه سازی و استفاده‌ی مجدد منتقل می‌کنند (اطلاعاتی مانند کوکی‌ها و تمام مشخصات جاری صفحه‌ی لاگین کرده). به عبارتی اگر شخصی به سایت لاگین کند و از طریق مرورگری که افزونه‌ی IDM بر روی آن نصب است، اقدام به دریافت فایل کند (بر روی لینک کلیک کند تا مرورگر توسط افزونه‌ی مربوطه، درخواست را به برنامه ارسال کند)، دریافت فایل از دید او معمولی و مانند قبل خواهد بود.
مطالب
آشنایی با ساختار IIS قسمت پنجم
در مطالب قبلی در مورد ماژولار بودن IIS زیاد صحبت کردیم، ولی اجازه بدهید این مورد را به صورت کاربردی‌تر و موشکافانه‌تر بررسی کنیم. برای اینکه به مشکلی در طول این سری از مطالب برنخورید، IIS را به صورت کامل یعنی full feature نصب نمایید. از بخش control panel>programs & features>Turn Windows features on or off اقدام نمایید و هرچه زیر مجموعه Internet information service هست را برگزینید. در صورتی که از نسخه‌های ویندوز سرور 2008 استفاده می‌کنید از طریق server manager>roles>web server اقدام نمایید.
برای نصب یک ماژول باید دو مرحله را انجام داد:
  1. نصب ماژول
  2. فعال سازی ماژول
نکته ای که در مورد ماژول‌های native وجود دارد این هست که این ماژول‌ها دسترسی بدون محدودیتی به منابع سروری دارند و از این رو حتما باید این نکته را دقت کنید که ماژول native شما از یک منبع مورد اعتماد دریافت شده باشد.

نصب یک native module
برای نصب می‌توانید یکی از سه راه زیر را استفاده کنید:
  1. ویرایش دستی فایل کانفیگ و از نسخه IIS7.5 به بعد هم می‌توانید از configuration editor هم استفاده کنید.
  2. استفاده از محیط گرافیکی IIS
  3. استفاده از خط فرمان با دستور Appcmd
مزیت روش دستی این هست که شما دقیقا می‌دانید در پشت صحنه چه اتفاقی می‌افتد و نتیجه هر کدام از این سه روش، اضافه شدن یک مدخل ورودی به تگ <globalmodules> است. برای اعمال تغییرات، مسیر زیر را بروید:
%windir%\system32\inetsrv\config\applicationhost.config
کسی که نیاز به دسترسی به این مسیر و انجام تغییرات دارد باید در بالاترین سطح مدیریتی سرور باشد.
اگر فایل را باز کنید و تگ globalmodule را پیدا کنید متوجه می‌شوید که تمامی ماژول‌ها در این قسمت معرفی شده‌اند و برای خود یک مدخل ورودی یا همان تگ add را دارند که در آن مسیر فایل dll هم ذکر شده است:
<globalModules> 
 
 <addname="DefaultDocumentModule"image="%windir%\system32\inetsrv\defdoc.dll"/> 
 
 <addname="DirectoryListingModule"image="%windir%\system32\inetsrv\dirlist.dll"/> 

<add name="StaticFileModule"image="%windir%\system32\inetsrv\static.dll"/>
...

 </globalModules>

برای حذف یا جایگزینی یک ماژول به راحتی می‌توانید مدخل ورودی یک ماژول را به صورت دستی حذف نمایید و برای جایگزینی هم بعد از حذف، ماژول خود را معرفی کنید. ولی توجه داشته باشید که این حذف به معنی حذف این ماژول از تمامی اپلیکیشن‌های موجود بر روی IIS هست و سپس اضافه کردن یک ماژول به این بخش. همچنین اگر قصد شما فقط حذف یک ماژول از روی یکی از اپلیکشن‌ها باشد باید از طریق فایل کانفیگ سایت از مسیر تگ‌های <system.webserver><modules> و با استفاده از تگ‌های add و remove به معرفی یک ماژول مختص این اپلیکیشن و یا حذف یک ماژول خاص اقدام نمایید.

PreConditions
این ویژگی می‌تواند در خط معرفی ماژول، مورد استفاده قرار بگیرد. اگر به فایل نگاه کنید می‌بینید که در بعضی خطوط این ویژگی ذکر شده است. تعریف این ویژگی به هسته IIS می‌گوید که این ماژول در چه مواردی و به چه شیوه ای باید به کار گرفته شود.
 <add name="ManagedEngine64" image="%windir%\Microsoft.NET\Framework64\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness64" /> 
 
 <add name="ManagedEngine" image="%windir%\Microsoft.NET\Framework\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness32" /> 
 
<add name="ManagedEngineV4.0_32bit" image="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness32" /> 
 
<add name="ManagedEngineV4.0_64bit" image="%windir%\Microsoft.NET\Framework64\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness64" />

مقادیری که precondition میتواند بگیر شامل موارد زیر هستند:
bitness
این گزینه به دو صورت bitness32 و bitness64 یافت می‌شود. امروزه پردازنده‌های 64 بیتی بسیار متداول شده اند و بسیاری از تولید کنندگان دارند به سمت عرضه ابزارهای 64 بیتی رو می‌آورند و به زودی عرضه‌های 32 بیتی را متوقف می‌کنند و به سمت سیستم عامل‌های 64 بیت سوییچ خواند کرد ولی باز هم هنوز برنامه‌های 32 بیتی زیادی هستند که مورد استفاده قرار می‌گیرند و نمی‌توان آن‌ها را نادیده گرفت. برای همین ویندوزهای 64 بیتی مایکروسافت در کنار محیط 64 بیتی‌شان از یک محیط 32 بیت به اسم WOW64 استفاده می‌کنند. در این حالت این امتیاز به شما داده می‌شود که از پروسه‌های کارگر 32 بیتی در کنار پروسه‌های کارگر 64 بیتی استفاده کنید و PreCondition به bitness32 یا bitness64 تنظیم می‌شود تا از صحت بارگزاری dll در یک محیط درست مطمئن شود. در صورتی که این خصوصیت ذکر نشود یک هندلر 32 بیتی و 64 بیتی و یک module map اجرا می‌شود.

Runtime version
اگر ماژول خاصی برای اجرا به ورژن خاصی از net framwork. نیاز دارد، این ویژگی ذکر می‌شود. در صورتی که ماژولی قصد اجرای بر روی فریم ورک اشتباهی داشته باشد سبب خطا خواهد شد.

Mana gedHandler 
با معرفی IIS7 ما با یک مدل توسعه پذیر روبرو شدیم و می‌توانستیم ماژول‌ها و هندلرهای خود را بنویسیم و مستقیما در Pipeline قرار دهیم ولی سوییچ کردن بین دو بخش کدهای مدیریت شده و native یک عمل سنگین برای سیستم به شمار می‌آید و به منظور کاهش این بار گزینه managedhandler قرار داده شده است تا تعیین کند مواقعی که درخواست نیازی به این ماژول ندارد، این ماژول اجرا نگردد. به عنوان مثال فایل‌های ایستا چون jpg یا html و... شامل این ماژول نخواهند شد. واضح‌ترین مثال در این زمینه Forms Authentication می‌باشد و موقعی اجرا می‌شوند که درخواست فایل‌های aspx شده باشد و اگر یک فایل html را درخواست کنید این ماژول امنیتی روی آن اثری ندارد و عملیات شناسایی هویت روی آن اجرا نمی‌شود و اگر می‌خواهید روی همه فایل‌ها، این عملیات شناسایی انجام شود باید خصوصیت "precondition="managedhandler حذف شود.
در صورتی که تگ module را به صورت زیر بنویسید تمامی ماژول‌ها برای تمامی درخواست‌ها اجرا خواهد شد، صرف نظر از اینکه آیا ماژولی به صورت "precondition="managedmodule مقداردهی شده است یا خیر.
<modules runAllManagedModulesForAllRequests="true"/>
 
The Mode Precondition 
تا به الان گفتیم که چگونه میتوانیم یک ماژول و یا هندلر مدیریت شده‌ای را به Pipeline اضافه کنیم؛ ولی IIS7 به بالا نیاز دارد که تا پروسه‌های کارگر را به روشی خاص به این منظور اجرا کند و فریم ورک دات نت را برای اجرای آن‌ها بارگزاری کند. همچنین به اجرای ماژولی به اسم webengine.dll برای مدیریت  ماژولهای مدیریت شده نیازمند است و خود IIS در مورد کدهای مدیریت شده چیزی متوجه نمی‌شود. پس ما برای اینکه IIS را متوجه این موضوع نمائیم، باید Integrated mode را به آن معرفی کنیم.
در نسخه‌های قبلی IIS یک روش قدیمی برای کدهای مدیریت شده وجود داشت که از طریق اینترفیسی به نام ISAPI صورت می‌گرفت. در این حالت ASPNET_ISAPI.DLL مسئول این کار بود و اگر هنوز هم میخواهید از این dll در نسخه‌های جدیدتر IIS کمک بگیرید باید به جای معرفی classic mode ، integration mode را معرفی کنید.
با برابر کردن precondtion به مقدار "intgretedmode" هندلر یا ماژول شما در یک پول با خصوصیت integrated بارگزاری خواهد شد و اگر مقدار آن "classicmode" باشد در یک پول بدون خاصیت integrated بارگزاری می‌شود.
تفاوت بین دو روش کلاسیک و مجتمع integrated بر سر این هست که در روش جدید، ماژول شما به عنوان یک پلاگین برای IIS دیده نمی‌شود و کد شما را جزئی از کامپوننت‌های خود به شمار می‌آورد. به صورت واضح‌تر در حالت کلاسیک موقعی که درخواستی وارد pipeline میشد ابتدا از کامپوننت‌ها و ماژول‌های داخلی خود IIS عبور داده میشد و بعد فایل ASPNET_ISAPI.DLL جهت پردازش کدهای مدیریت شده صدا زده میشد و با توجه به کدهای شما، بعضی مراحل تکرار میشد؛ مثلا اگر کد شما در مورد Authentication بود و بعد از گذر از مراحل auth داخل خود IIS و بقیه موارد دوباره نوبت کد شما و گذر از مراحل authentication بود. یعنی وجود دو pipeline؛ ولی در حالت مجتمع این دوبار انجام وظیفه از بین رفته است چرا که کدهای شما به طور مستقیم در pipeline قرار دارند و آن‌ها را جزئی از خود می‌داند، نه یک پلاگین که افزون بر فعالیت خودشان، اجرای کدهای شما رو هم بر دوش بکشند.
شکل زیر نمونه ای از حالت کلاسیک را نشان می‌دهد که در آن دو بار عمل auth دارد انجام میگیرد.
 



شکل زیر هم نمونه ای حالت مجتمع هست:

 
در کل امروزه دیگر استفاده از روش کلاسیک راهکار درستی نیست و این ویژگی تنها به عنوان یک سازگاری با نمونه کارهای قدیمی است.
تگ‌هایی که از خصوصت precondition استفاده می‌کنند به شرح زیر هستند:
  • ISAPI filters 
  • globalModules 
  • handlers 
  • modules 

در مورد بقیه تگ‌ها در آینده بیشتر بحث می‌کنیم. بهتر هست مطلب را با توضیح precondition جهت ممانعت از طولانی و طومار شدن در اینجا ببندیم و در قسمت آینده دیگر روش‌های نصب ماژولمان را دنبال کنیم.
مطالب
آموزش PouchDB : قسمت دوم (شروع به کار)

در این آموزش هدف ما ایجاد برنامه‌ای بر اساس TodoMVC است که می‌تواند خودش را با یک دیتابیس آنلاین همگام سازی کند.

مطمئن باشید بیشتر از 10 دقیقه وقت شمارا نخواهد گرفت !

نصب PouchDB

فایل index.html  را باز کنید و فایل‌های PouchDB را به آن اضافه کنید :

<script src="//cdn.jsdelivr.net/pouchdb/2.2.0/pouchdb.min.js"></script>
<script src="js/base.js"></script>
<script src="js/app.js"></script>

حالا PouchDB نصب شده و آماده به کار است . ( در نسخه نهایی بهتر است از فایل‌های local استفاده کنید )

ایجاد بانک اطلاعاتی

بقیه کارها باید در فایل app.js انجام شود . برای شروع باید بانک اطلاعاتی جدیدی را برای درج اطلاعات خودمان ایجاد کنیم . برای ایجاد بانک اطلاعاتی خیلی ساده یک instance جدید از آبجکت PouchDB می‌سازیم .

var db = new PouchDB('todos');
var remoteCouch = false;

نیازی نیست که برای بانک خود، نما (Schema) ایجاد کنید! بعد از اینکه اسم را مشخص کنید، بانک آماده به کار است.

ثبت اطلاعات در بانک اطلاعاتی

اولین کاری که باید انجام دهیم این است که یک متد را ایجاد کنیم و توسط آن اطلاعات خودمان را در بانک اطلاعاتی ذخیره کنیم. نام متد را addTodo انتخاب می‌کنیم و کارش این است که وقتی کاربر کلید Enter را فشار داد، اطلاعات را داخل بانک اطلاعاتی ذخیره کند. متد مورد نظر به این صورت هست:

function addTodo(text) {
  var todo = {
    _id: new Date().toISOString(),
    title: text,
    completed: false
  };
  db.put(todo, function callback(err, result) {
    if (!err) {
      console.log('Successfully posted a todo!');
    }
  });
}

در PouchDB هر پرونده نیاز دارد تا یک فیلد unique با نام _id داشته باشد. اگر داده‌ای بخواهد در بانک اطلاعاتی ثبت شود و دارای _id مشابه باشد، با آن به صورت یک update رفتار خواهد شد. در اینجا ما از تاریخ با عنوان id استفاده کردیم که در این مورد خاص می‌باشد. شما می‌تواند از db.post() نیز استفاده کنید؛ اگر یک id را به صورت random می‌خواهید. تنها چیزی که اجباری است در هنگام ساختن یک پرونده، همین _id است و بقیه موارد کاملا اختیاری هستند.

نمایش اطلاعات

در اینجا ما از یک function کمکی به نام redrawTodosUI استفاده خواهیم کرد که وظیفه‌اش این است تا یک array را دریافت کرده و آن را هر طور که مشخص کردید نمایش دهد. البته آن را به سلیقه خودتان می‌توانید آماده کنید.
تنها کاری که باید انجام دهیم این است که اطلاعات را از بانک اطلاعاتی استخراج کرده و به function مورد نظر پاس دهیم.
در اینجا می‌توانیم به سادگی از db.allDocs برای خواندن اطلاعات از بانک اطلاعاتی استفاده کنیم.
خاصیت include_docs به PouchDB این دستور را می‌دهد که ما درخواست دریافت همه اطلاعات داخل پرونده‌ها را داریم و descending هم نحوه مرتب سازی را که بر اساس _id هست، مشخص می‌کند تا بتوانیم جدیدترین اطلاعات را اول دریافت کنیم .

function showTodos() {
  db.allDocs({include_docs: true, descending: true}, function(err, doc) {
    redrawTodosUI(doc.rows);
  });
}

به روزرسانی خودکار

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

var remoteCouch = false;

db.info(function(err, info) {
  db.changes({
    since: info.update_seq,
    live: true
  }).on('change', showTodos);
});

حالا هر بار که اطلاعات جدیدی در بانک اطلاعات ثبت شود، به صورت خودکار نمایش داده خواهد شد. خاصیت live مشخص می‌کند که این کار می‌تواند بی نهایت بار انجام شود.

ویرایش اطلاعات

وقتی که کاربر یک آیتم Todo را کامل می‌کند، یک چک باکس را علامت می‌زند و اعلام می‌کند که این کار انجام شده است.

function checkboxChanged(todo, event) {
  todo.completed = event.target.checked;
  db.put(todo);
}

این بخش شبیه به قسمت ثبت اطلاعات است، با این تفاوت که باید شامل یک فیلد _rev ( اضافه بر _id ) نیز باشد. در غیر اینصورت تغییرات ثبت نخواهد شد. این کار برای این است که اشتباهی، اطلاعاتی در بانک ثبت نشود.

حذف اطلاعات

برای حذف اطلاعات باید از متد db.remove به همراه آبجکت مورد نظر استفاده کنید .

function deleteButtonPressed(todo) {
  db.remove(todo);
}

مانند بخش ویرایش نیز باید در اینجا هم _id و هم _rev مشخص شود.
باید توجه داشته باشید در اینجا همان آبجکتی را که از بانک فراخوانی کرده‌ایم، به این متد پاس می‌دهیم.
البته شما می‌تونید آبجکت خودتان را نیز ایجاد کنید {_id: todo._id, _rev: todo._rev} اما خوب همان روش قبلی عاقلانه‌تر است و احتمال خطای کمتری دارد .

نصب CouchDB

حالا می‌خواهیم همگام سازی را اجرا کنیم و برای این کار نیاز هست یا CouchDB را به صورت Local نصب کنیم یا از سرویس‌های آنلاین مثل IrisCouch استفاده کنید .

فعال سازی CORS

برای اینکه به صورت مستقیم با CouchDB در ارتباط باشید باید مطمئن شوید که CORS فعال هست.
در اینجا فقط نام کاربری و رمز ورود را مشخص کنید. به صورت پیش فرض CouchDB به صورت Admin Party نصب می‌شود و نیازی به User و password ندارد. مگر اینکه برایش مشخص کنید.
همچنین شما باید myname.iriscouch.com را با سرور خودتان جایگزین کنید که اگر به صورت local کار می‌کنید 127.0.0.1:5984 است.

$ export HOST=http://username:password@myname.iriscouch.com
$ curl -X PUT $HOST/_config/httpd/enable_cors -d '"true"'
$ curl -X PUT $HOST/_config/cors/origins -d '"*"'
$ curl -X PUT $HOST/_config/cors/credentials -d '"true"'
$ curl -X PUT $HOST/_config/cors/methods -d '"GET, PUT, POST, HEAD, DELETE"'
$ curl -X PUT $HOST/_config/cors/headers -d \
  '"accept, authorization, content-type, origin"'

راه اندازی ارتباط ساده دو طرفه

به فایل app.js برگردید . در اینجا باید آدرس بانک اطلاعاتی آنلاین را مشخص کنیم.

var db = new PouchDB('todos');
var remoteCouch = 'http://user:pass@mname.iriscouch.com/todos';

فراموش نکنید که نام کاربری و رمز ورود را بسته به نیاز خود تغییر دهید.
حالا می‌تونیم function که وظیفه همگام سازی اطلاعات را به عهده دارد بنویسیم :

function sync() {
  syncDom.setAttribute('data-sync-state', 'syncing');
  var opts = {live: true};
  db.replicate.to(remoteCouch, opts, syncError);
  db.replicate.from(remoteCouch, opts, syncError);
}

db.replicate() به PouchDB می‌گوید که که همه اطلاعات را "به" یا "از" remoteCouch انتقال دهد.
ما دوبار این درخواست را دادیم. در بار اول اطلاعات به داخل سرور ریموت منتقل می‌شود و در بار دوم اطلاعات local جایگزین می‌شوند.
یک callback هم وقتی که این کار به پایان برسد اجرا خواهد شد .
می‌توانید برنامه خود را در دو مرورگر مختلف اجرا کنید تا نتیجه کار را ببینید.

تبریک می‌گوییم !

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

مطالب
کار با بانک‌های اطلاعاتی فاکس‌پرو و OleDB در دات نت 7
فرض کنید قصد خواندن اطلاعات یک بانک اطلاعاتی قدیمی فاکس‌پرو را با آخرین نگارش دات نت دارید. اگر سعی کنید از روش‌های و مطالب موجود استفاده کنید، هیچکدام جواب نخواهند داد! در این مطلب تغییرات صورت گرفته را بررسی می‌کنیم.


نیاز به درایور OleDB مخصوص بانک‌های اطلاعاتی قدیمی

برای کار با بانک‌های اطلاعاتی قدیمی از طریق ADO.NET، نیاز است بتوان به نحوی با آن‌ها ارتباط برقرار کرد و اینکار از طریق استاندارد OleDB که صرفا مختص به ویندوز است، قابل انجام است. برای مثال برای کار با فاکس‌پرو نیز در ابتدا باید درایور OleDB آن‌را نصب کرد که ... هیچکدام از لینک‌های قدیمی مایکروسافت در این زمینه دیگر وجود خارجی ندارند! آخرین نگارش مرتبط را می‌توانید در این آدرس و ذیل نام VFPOLEDBSetup.msi دریافت کنید (نگارش 9 را نصب می‌کند).


نیاز به دریافت بسته‌ی System.Data.OleDb

در اولین قدم جهت کار با درایور OleDB نصب شده، باید یک اتصال را توسط نمونه سازی شیء OleDbConnection ایجاد کرد که ... این شیء هم شناسایی نمی‌شود. به همین جهت باید بسته‌ی نیوگت مرتبط با آن‌را به صورت جداگانه‌ای دریافت و نصب کرد:
<ItemGroup>
   <PackageReference Include="System.Data.OleDb" Version="7.0.0"/>
</ItemGroup>


برنامه‌ی مبتنی بر درایور OleDB فاکس‌پرو اجرا نمی‌شود!

اولین سعی در برقراری ارتباط با درایور OleDB نصب شده، با خطای زیر خاتمه می‌یابد:
The 'VFPOLEDB' provider is not registered on the local machine.
مشکل اینجا است که درایور ارائه شده، 32 بیتی است و ما سعی داریم آن‌را در یک محیط 64 بیتی اجرا کنیم. به همین جهت خطای فوق ظاهر می‌شود. برای رفع آن باید PlatformTarget را به x86 تنظیم کرد:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
   <PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
البته این تنظیم هم اگر پیشتر تنها runtime و یا SDK پیش‌فرض 64 بیتی را نصب کرده‌اید، سبب اجرای برنامه نخواهد شد. برای فعالسازی آن باید SDK x86 را هم جداگانه دریافت و نصب کنید.



روش یافتن نام پروایدر OleDB نصب شده

رشته‌های اتصالی OleDB، با =Provider، شروع می‌شوند. اکنون این سؤال مطرح می‌شود که پس از نصب VFPOLEDBSetup.ms یاد شده، دقیقا چه پروایدری به سیستم اضافه شده‌است و نام آن چیست؟
برای اینکار می‌توان از چندسطر زیر برای یافتن نام تمام پروایدرهای OleDB نصب شده‌ی بر روی سیستم استفاده کرد:
var oleDbEnumerator = new OleDbEnumerator();
using var elements = oleDbEnumerator.GetElements();
foreach (DataRow row in elements.Rows)
{
    Console.WriteLine($"{row["SOURCES_DESCRIPTION"]}: {row["SOURCES_NAME"]}");
}
که برای مثال یک خروجی آن می‌تواند به صورت زیر باشد:
Microsoft OLE DB Provider for SQL Server: SQLOLEDB
MSDataShape: MSDataShape
SQL Server Native Client 11.0: SQLNCLI11
Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB
OLE DB Provider for Microsoft Directory Services: ADsDSOObject
Microsoft OLE DB Driver for SQL Server: MSOLEDBSQL
Microsoft OLE DB Driver for SQL Server Enumerator: MSOLEDBSQL Enumerator
SQL Server Native Client 11.0 Enumerator: SQLNCLI11 Enumerator
Microsoft OLE DB Provider for Search: Windows Search Data Source
Microsoft OLE DB Provider for ODBC Drivers: MSDASQL
Microsoft OLE DB Enumerator for ODBC Drivers: MSDASQL Enumerator
Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP
Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP
Microsoft Jet 4.0 OLE DB Provider: Microsoft.Jet.OLEDB.4.0
Microsoft OLE DB Enumerator for SQL Server: SQLOLEDB Enumerator
Microsoft OLE DB Simple Provider: MSDAOSP
Microsoft OLE DB Provider for Oracle: MSDAORA
که در اینجا ما به دنبال یک سطر زیر هستیم:
Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB
یعنی نام دقیق پروایدر مرتبط با فاکس‌پرو، VFPOLEDB است.


روش خواندن اطلاعات یک بانک اطلاعاتی فاکس پرو

پس از این مقدمات و تنظیمات، اکنون می‌توانیم از قطعه کد متداول ADO.NET زیر، جهت خواندن اطلاعات یک بانک اطلاعاتی فاکس‌پرو، استفاده کنیم:
var connectionString = "Provider=VFPOLEDB;Data Source=" +
       @"C:\path\Db.DBF;Password=;Collating Sequence=MACHINE";
using var dbConnection = new OleDbConnection(connectionString);
using var dataAdapter = new OleDbDataAdapter("select family from Db.DBF", dbConnection);
using var dataset = new DataSet();
dataAdapter.Fill(dataset, "table1");

var sb = new StringBuilder();
foreach (DataRow dataRow in dataset.Tables[0].Rows)
{
    var iranSystem = dataRow[0] as string;
    var unicode = iranSystem.FromIranSystemToUnicode();
    if (!string.IsNullOrWhiteSpace(unicode))
    {
       sb.AppendLine($"[DataRow(\"{iranSystem}\", \"{unicode}\")]");
    }
}
توضیحات:
- نام پروایدر در رشته اتصالی، به VFPOLEDB تنظیم شده‌است.
- select انجام شده بر روی نام فایل dbf انجام می‌شود. یعنی هر فایل dbf، یک جدول را تشکیل می‌دهد.
- اگر نام فیلدهای موجود را نمی‌دانید، بجای select family از * select استفاده کنید و سپس بر روی DataSet پر شده، یک break-point را قرار دهید تا بتوانید نام تمام ستون‌ها را از آن استخراج کنید.
- رشته‌ای را که توسط درایور فاکس‌پرو دریافت می‌کنید، یک رشته‌ی اسکی سیستم عامل داس است که در دات نت، با encoding مساوی 1252 شناخته می‌شود. یعنی encoding این رشته‌ی دریافتی، یونیکد پیش‌فرض دات‌نت نیست و باید توسط متد Encoding.GetEncoding(1252).GetBytes پردازش شود. که در نگارش‌های جدید دات نت، این کد‌پیج‌ها به صورت پیش‌فرض مهیا نیستند و باید در ابتدا ثبت شوند تا قابل استفاده شوند:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
- متد FromIranSystemToUnicode استفاده شده، جزئی از «DNTPersianUtils.Core» است که در صورت نیاز به تبدیل اطلاعات قدیمی ایران سیستمی به یونیکد، می‌تواند مورد استفاده قرار گیرد.
مطالب دوره‌ها
ابزاری برای تولید کدهای Reflection.Emit
برنامه معروف Reflector دارای افزونه‌ای است به نام Reflector.ReflectionEmitLanguage که سورس آن از آدرس ذیل قابل دریافت است:


مشخصات آن‌را نیز در آدرس زیر می‌توانید مشاهده نمائید:

به این ترتیب به منوی انتخاب زبان‌های Reflector، یک زبان جدید به نام ReflectionEmit اضافه خواهد شد:




مشکل!
این افزونه مدت زیادی است که به روز نشده و با آخرین نگارش Reflector سازگار نیست. برای رفع این مشکل ابتدا سورس آن‌را از کدپلکس دریافت و سپس تغییرات ذیل را به آن اعمال کنید:
الف) به قسمت ارجاعات پروژه افزونه مراجعه و ارجاع به Reflector قدیمی آن‌را حذف و آدرس فایل exe برنامه Reflector جدید را به عنوان ارجاعی تازه، ثبت کنید.
ب) در فایل Visitor.cs آن باید تغییر کوچکی در متد ذیل به نحوی که مشاهده می‌کنید صورت گیرد:
public virtual void VisitOrderClause(IOrderClause value)
{
    this.VisitOrderClause(value);
}
پس از آن، پروژه را کامپایل کرده و فایل dll حاصل را در پوشه Addins نگارش جدید Reflector کپی کنید. سپس به منوی Tools و گزینه Addins در برنامه مراجعه کرده و آدرس فایل Reflector.ReflectionEmitLanguage.dll را برای معرفی به برنامه مشخص نمائید.
به این ترتیب نگارش قدیمی افزونه Reflector.ReflectionEmitLanguage.dll با نگارش جدید برنامه Reflector سازگار خواهد شد.
سورس تغییر یافته این افزونه را از اینجا نیز می‌توانید دریافت کنید:
بدیهی است به ازای هر نگارش جدید Reflector، یکبار باید قسمت الف توضیحات فوق تکرار شود.
مطالب
آموزش MDX Query - قسمت سوم - نصب Adventure Work DW و تهیه ی پایگاه داده ی Multidimensional Database توسط SSAS

برای ادامه دادن این سری از مقالات آموزش MDX Query نیاز می‌باشد که پایگاه داده‌ی Advwnture Work DW را نصب کرده و سپس توسط SSAS عمل Deploy را انجام دهیم تا پایگاه داده‌ی Multidimensional Database توسط SSAS ساخته شود .

در ابتدا می‌بایست فایل نصب پایگاه داده ی  Advwnture Work را دانلود نمایید برای این منظور به آدرس زیر رفته و فایل AdventureWorks2008R2_SR1.exe  را دانلود نمایید .

  http://www.general-files.biz/download/gs4ac37d18h17i0/AdventureWorks2008R2_SR1.exe.html

یا به آدرس زیر مراجعه کنید

https://msftdbprodsamples.codeplex.com/releases/view/59211 

نیاز می‌باشد قبل از شروع به نصب نرم افزار SQL Server Management Studio را ببندید.

سپس مراحل زیر را انجام دهید.

1. فایل AdventureWorks2008R2_SR1.exe را اجرا نمایید. 

2.  کمی صبر کنید تا صفحه‌ی زیر نمایش داده شود. و گزینه‌ی I Accept … را انتخاب نماید و دکمه‌ی Next را بزنید. 

3. در صورتی که از ویندوز 8 استفاده نماید احتمال دارد با خطای زیر مواجه شوید در این صورت به قسمت روش نصب در ویندوز 8 در ادامه‌ی این مقاله مراجعه کنید . 

4. در صورتی که از ویندوز‌های Server 2003, XP , Win7 استفاده کنید صفحه‌ی زیر را خواهید دید. در این صفحه ابتدا Instance مربوط به SQL سرور خود را انتخاب نماید (در صورت داشتن چندین Instance روی سرور پایگاه داده) سپس مسیر نصب فایل‌های Sample را مشخص نمایید (بعدا از همین مسیر اقدام به Deploy کردن پایگاه داده‌ی Multidimensional خواهیم کرد) و پیش فرض‌ها را بپذیرید و دکمه‌ی Install را بزنید. 

5. کمی صبر کنید تا نصب انجام گردد. و در انتها کلید Finish را بزنید. 

پس از مراحل بالا (به جز ویندوز 8) با باز کردن نرم افزار SQL Server Management Studio و اتصال به سرویس Database Engine در قسمت Database تصویر زیر را خواهید دید (البته امکان دارد شما از قبل دارای پایگاه داده‌های شخصی بوده باشید که بنابر این آنها نیز در لیست شما وجود خواهند داشت)  

همچنین شما می‌توانید از پنجره‌ی Object Explorer در قسمت Connect اقدام به اتصال به سرویس SSAS نموده . 

و در پنجره‌ی باز شده Server Name را انتخاب نمایید (با توجه به اینکه شما در حال حاضر می‌خواهید به SSAS موجود در سیستم Local متصل شوید ، بنابر این انتخاب سرور Local با وارد کردن کاراکتر (.) انجام می‌شود.) 

بعد از اتصال شکل زیر را خواهید داشت و در شاخه‌ی Database همچنان هیچ Multidimensional Database ی نخواهید داشت.(بعد از عمل Deploy که در ادامه آموزش داده خواهد شد پایگاه داده‌ی Multidimensional ساخته می‌شود.) 

 

تنها روشی که تاکنون برای  نصب پایگاه داده‌ی Adventure Work DW برروی ویندوز 8 یافته ام (البته کمی غیر حرفه ای می‌باشد.) به صورت زیر می‌باشد.

فایل بالا را ( AdventureWorks2008R2_SR1.exe ) روی سیستم عامل‌های ( Server 2003,XP,Win 7 ) نصب کرده (به عنوان یک سیستم عامل واسط) و سپس سرویس Database Engine را Stop کرده و فایل‌های پایگاه داده را به سیستم عامل ویندوز 8 انتقال داده و به صورت دستی Restore کنیم.

مراحل ایجاد پایگاه داده‌ی Multidimensional در ویندوز‌های مختلف ، یکسان می‌باشد.

 

بعد از نصب پایگاه داده‌ی Adventure Work DW باید به شاخه‌ی نصب Sample بروید (همان مسیری که در مراحل نصب وارد کردیم و البته آدرس پیش فرض آن C:\Program Files\Microsoft Sql Server\100\Tools\Sample می‌باشد.)

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

سپس به زیر شاخه‌ی \ AdventureWorks 2008R2 Analysis Services Project\enterprise بروید و فایل Adventure Works.sln را با Visual Studio 2010 باز کنید.

احتمال دارد که نیاز باشد روی کل شاخه‌ی enterprise در قسمت Security کاربر جاری را Add کنید و به آن دسترسی Full Control بدهید تا عملیات Convert این پروژه به درستی انجام شود.

پس از باز کردن پروژه در Visual Studio 2010 صفحه ای مطابق تصویر زیر در پنجره‌ی Solution Explorer خواهید دید. 

به هیچ عنوان نگران ساختار این پروژه نباشید ، زیرا در مقاله‌های آیند شرح کاملی در این خصوص کار با Business Intelligence Management Studio خواهم داد. فعلا هدف ما ایجاد پایگاه داده‌ی Multidimensional  می باشد.

 

برای ساخت پایگاه داده‌ی Multidimensional مراحل زیر را دنبال نمایید.

1. در ابتدا روی پروژه کلیک راست کرده و گزینه‌ی Properties را انتخاب نمایید. 

2. در قسمت Configuration Properties  منوی Deployment را انتخاب کرده و اطمینان حاصل کنید که سرور شما LocalHost و نام پایگاه داده شما Adventure Works DW 2008R2  باشد. 

3. سپس روی Adventure Works.ds کلیک راست کنید تا تنظیمات Connection String  به DW را انجام دهیم. مطابق شکل زیر 

4. سپس در پنجره‌ی باز شده دکمه‌ی Edit  را بزنید . 

5. و در صفحه باز شده تنظیمات زیر را مطابق تصویر زیر انجام دهید. دقت داشته باشید که تغییرات را از بالا به پایین باید انجام دهید و قبل از زدن دکمه‌ی OK حتما Test Connection را بزنید تا از صحت تنظیمات مطمعا شوید. 

6. سپس دو بار دکمه‌ی OK را در دوصفحه کلیک کنید. (بعد از این مراحل شما آماده‌ی Deploy کردن می‌باشد)

7. در ابتدا پروژه را Build نمایید ( CTRl + Shift + B )  و اطمینان حاصل کنید که Build  با موفقیت انجام می‌شود.

8. در انتها برروی نام پروژه کلیک راست نمایید و گزینه‌ی Deploy را انتخاب نمایید. فرایند Deploy کردن می‌تواند کمی زمان بر باشد بنابر این شکیبا باشید و در انتها پیام Deployment Completed Successfully را دریافت خواهید کرد. 

9. حال به SQL Server Management Studio بروید و به سرویس SSAS کانکت شوید . در قسمت DataBase یک پایگاه داده با نام Adventure Works DW 2008R2 مشاهده خواهید کرد . 

 

به شما تبریک می‌گویم اینک شما یک پایگاه داده‌ی Multidimensional را ساخته اید .

در مقاله‌ی بعدی توضیحاتی در خصوص BIMS (Business Intelligence Management Studio) خواهم داد و همچنین اولین MDX Query  را خواهیم نوشت.

 

مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت نهم - مثالی از کتابخانه‌ی mobx-react
در ادامه‌ی سری کار با MobX، می‌خواهیم نکاتی را که در سه قسمت قبل مرور کردیم، در قالب یک برنامه پیاده سازی کنیم:


این برنامه از چهار کامپوننت تشکیل شده‌است:
- کامپوننت App که در برگیرنده‌ی سه کامپوننت زیر است:
- کامپوننت BasketItemsCounter: جمع تعداد آیتم‌های انتخابی توسط کاربر را نمایش می‌دهد؛ به همراه دکمه‌ای برای خالی کردن لیست انتخابی.
- کامپوننت ShopItemsList: لیست محصولات موجود در فروشگاه را نمایش می‌دهد. با کلیک بر روی هر آیتم آن، آیتم انتخابی به لیست انتخاب‌های او اضافه خواهد شد.
- کامپوننت BasketItemsList: لیستی را نمایش می‌دهد که حاصل انتخاب‌های کاربر در کامپوننت ShopItemsList است (یا همان سبد خرید). در ذیل این لیست، جمع نهایی قیمت قابل پرداخت نیز درج می‌شود. همچنین اگر کاربر بر روی دکمه‌ی remove هر ردیف کلیک کند، یک واحد از چند واحد انتخابی، حذف خواهد شد.

بنابراین در اینجا سه کامپوننت مجزا را داریم که با هم تبادل اطلاعات می‌کنند. یکی جمع تعداد محصولات خریداری شده را، دیگری لیست محصولات موجود را و آخری لیست خرید نهایی را نمایش می‌دهد. همچنین این سه کامپوننت، فرزند یک دیگر هم محسوب نمی‌شوند و انتقال اطلاعات بین این‌ها نیاز به بالا بردن state هر کدام و قرار دادن آن‌ها در کامپوننت App را دارد تا بتوان پس از آن از طریق props آن‌ها را بین سه کامپوننت فوق که اکنون فرزند کامپوننت App محسوب می‌شوند، به اشتراک گذاشت. روش بهتر اینکار، استفاده از یک مخزن حالت سراسری است تا حالت‌های این کامپوننت‌ها را نگهداری کرده و داده‌‌ها را بین آن‌ها به اشتراک بگذارد که در اینجا برای حل این مساله از کتابخانه‌های mobx و mobx-react استفاده خواهیم کرد.


برپایی پیش‌نیازها

برای پیاده سازی برنامه‌ی فوق، یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> create-react-app state-management-with-mobx-part4
> cd state-management-with-mobx-part4
در ادامه کتابخانه‌ها‌ی زیر را نیز در آن نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save bootstrap mobx mobx-react mobx-react-devtools mobx-state-tree
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود.
- اصل کار برنامه توسط دو کتابخانه‌ی mobx و کتابخانه‌ی متصل کننده‌ی آن به برنامه‌های react که mobx-react نام دارد، انجام خواهد شد.
- چون می‌خواهیم از افزونه‌ی  mobx-devtools نیز استفاده کنیم، نیاز است دو بسته‌ی mobx-react-devtools و همچنین mobx-state-tree را که جزو وابستگی‌های آن است، نصب کنیم.

سپس بسته‌های زیر را که در قسمت devDependencies فایل package.json درج خواهند شد، باید نصب شوند:
> npm install --save-dev babel-eslint customize-cra eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort react-app-rewired typescript
علت آن‌را در قسمت قبل بررسی کردیم. این وابستگی‌ها برای فعالسازی react-app-rewired و همچنین eslint غنی سازی شده‌ی آن مورد استفاده قرار می‌گیرند. به علاوه سه قسمت زیر را نیز از قسمت قبل، به پروژه اضافه می‌کنیم:
- افزودن فایل جدید config-overrides.js به ریشه‌ی پروژه، تا پشتیبانی ازlegacy" decorators spec" فعال شود.
- اصلاح فایل package.json و ویرایش قسمت scripts آن برای استفاده‌ی از react-app-rewired، تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود.
- همچنین فایل غنی شده‌ی eslintrc.json. را نیز به ریشه‌ی پروژه اضافه می‌کنیم.


تهیه سرویس لیست محصولات موجود در فروشگاه

این برنامه از یک لیست درون حافظه‌ای، برای تهیه‌ی لیست محصولات موجود در فروشگاه استفاده می‌کند. به همین جهت پوشه‌ی service را افزوده و سپس فایل جدید src\services\productsService.js را با محتوای زیر، ایجاد می‌کنیم:
const products = [
  {
    id: 1,
    name: "Item 1",
    price: 850
  },
  {
    id: 2,
    name: "Item 2",
    price: 900
  },
  {
    id: 3,
    name: "Item 3",
    price: 1500
  },
  {
    id: 4,
    name: "Item 4",
    price: 1000
  }
];

export default products;


ایجاد کامپوننت نمایش لیست محصولات


پس از مشخص شدن لیست محصولات قابل فروش، کامپوننت جدید src\components\ShopItemsList.jsx را به صورت زیر ایجاد می‌کنیم:
import React from "react";

import products from "../services/productsService";

const ShopItemsList = ({ onAdd }) => {
  return (
    <table className="table table-hover">
      <thead className="thead-light">
        <tr>
          <th>Name</th>
          <th>Price</th>
          <th>Action</th>
        </tr>
      </thead>
      <tbody>
        {products.map(product => (
          <tr key={product.id}>
            <td>{product.name}</td>
            <td>{product.price}</td>
            <td>
              <button
                className="btn btn-sm btn-info"
                onClick={() => onAdd(product)}
              >
                Add
              </button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

export default ShopItemsList;
- این کامپوننت آرایه‌ی products را از طریق سرویس services/productsService دریافت کرده و سپس با استفاده از متد Array.map، حلقه‌ای را بر روی عناصر آن تشکیل داده که در نتیجه، سبب درج trهای متناظر با آن می‌شود؛ تا هر ردیف این جدول، یک آیتم از محصولات موجود را نیز نمایش دهد.
- در اینجا همچنین هر ردیف، به همراه یک دکمه‌ی Add نیز هست که قرار است با کلیک بر روی آن، متد رویدادگردان onAdd فراخوانی شود. این متد نیز از طریق props این کامپوننت دریافت می‌شود. کتابخانه‌های مدیریت حالت، تمام خواص و رویدادگردان‌های مورد نیاز یک کامپوننت را از طریق props، تامین می‌کنند.
- فعلا این کامپوننت به هیچ مخزن داده‌ای متصل نیست و فقط طراحی ابتدایی آن آماده شده‌است.


ایجاد کامپوننت نمایش لیست خرید کاربر (سبد خرید)


اکنون که می‌توان توسط کامپوننت لیست محصولات، تعدادی از آن‌ها را خریداری کرد، کامپوننت جدید src\components\BasketItemsList.jsx را برای نمایش لیست نهایی خرید کاربر، به صورت زیر پیاده سازی می‌کنیم:
import React from "react";

const BasketItemsList = ({ items, totalPrice, onRemove }) => {
  return (
    <>
      <table className="table table-hover">
        <thead className="thead-light">
          <tr>
            <th>Name</th>
            <th>Price</th>
            <th>Count</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          {items.map(item => (
            <tr key={item.id}>
              <td>{item.name}</td>
              <td>{item.price}</td>
              <td>{item.count}</td>
              <td>
                <button
                  className="btn btn-sm btn-danger"
                  onClick={() => onRemove(item.id)}
                >
                  Remove
                </button>
              </td>
            </tr>
          ))}

          <tr>
            <td align="right">
              <strong>Total: </strong>
            </td>
            <td>
              <strong>{totalPrice}</strong>
            </td>
            <td></td>
            <td></td>
          </tr>
        </tbody>
      </table>
    </>
  );
};

export default BasketItemsList;
- عملکرد این کامپوننت نیز شبیه به کامپوننت نمایش لیست محصولات است؛ با این تفاوت که لیستی که به آن از طریق props ارسال می‌شود:
const BasketItemsList = ({ items, totalPrice, onRemove }) => {
لیست محصولات انتخابی کاربر است.
- همچنین هر ردیف نمایش داده شده، به همراه یک دکمه‌ی Remove آیتم انتخابی نیز هست که به متد رویدادگردان onRemove متصل شده‌است.
- در ردیف انتهایی این لیست، مقدار totalPrice که یک خاصیت محاسباتی است، درج می‌شود.
- فعلا این کامپوننت نیز به هیچ مخزن داده‌ای متصل نیست و فقط طراحی ابتدایی آن آماده شده‌است.


ایجاد کامپوننت نمایش تعداد آیتم‌های خریداری شده


کاربر اگر آیتمی را از لیست محصولات انتخاب کند و یا محصول انتخاب شده را از لیست خرید حذف کند، تعداد نهایی باقی مانده را می‌توان در کامپوننت src\components\BasketItemsCounter.jsx مشاهده کرد:
import React, { Component } from "react";

class BasketItemsCounter extends Component {
  render() {
    const { count, onRemoveAll } = this.props;
    return (
      <div>
        <h1>Total items: {count}</h1>
        <button
          type="button"
          className="btn btn-sm btn-danger"
          onClick={() => onRemoveAll()}
        >
          Empty Basket
        </button>
      </div>
    );
  }
}

export default BasketItemsCounter;
- این کامپوننت یک خاصیت و یک رویدادگردان را از طریق props خود دریافت می‌کند. خاصیت count، جمع نهایی موجود در سبد خرید را نمایش می‌دهد و فراخوانی onRemoveAll، سبب پاک شدن تمام آیتم‌های موجود در سبد خرید خواهد شد.
- فعلا این کامپوننت نیز به هیچ مخزن داده‌ای متصل نیست و فقط طراحی ابتدایی آن آماده شده‌است.


نمایش ابتدایی سه کامپوننت توسط کامپوننت App

اکنون که این سه کامپوننت تکمیل شده‌اند، می‌توان المان‌های آن‌ها را در فایل src\App.js درج کرد تا در صفحه نمایش داده شوند:
import React, { Component } from "react";

import BasketItemsCounter from "./components/BasketItemsCounter";
import BasketItemsList from "./components/BasketItemsList";
import ShopItemsList from "./components/ShopItemsList";

class App extends Component {
  render() {
    return (
      <main className="container">
        <div className="row">
          <BasketItemsCounter />
        </div>

        <hr />

        <div className="row">
          <h2>Products</h2>
          <ShopItemsList />
        </div>

        <div className="row">
          <h2>Basket</h2>
          <BasketItemsList />
        </div>
      </main>
    );
  }
}

export default App;


طراحی مخزن‌های حالت MobX مخصوص برنامه


می‌توان همانند Redux کل state برنامه را داخل یک شیء store ذخیره کرد و یا چون در اینجا می‌توان طراحی مخزن حالت MobX را به دلخواه انجام داد، می‌توان چندین مخزن حالت را تهیه و به هم متصل کرد؛ مانند تصویری که مشاهده می‌کنید. در اینجا:
- src\stores\counter.js: مخزن داده‌ی حالت کامپوننت شمارشگر است.
- src\stores\market.js: مخزن داده‌ی کامپوننت‌های لیست محصولات و سبد خرید است.
- src\stores\index.js: کار ترکیب دو مخزن قبل را انجام می‌دهد.

در ادامه کدهای کامل این مخازن را مشاهده می‌کنید:

مخزن حالت src\stores\counter.js
import { action, observable } from "mobx";

export default class CounterStore {
  @observable totalNumbersInBasket = 0;

  constructor(rootStore) {
    this.rootStore = rootStore;
  }

  @action
  increase = () => {
    this.totalNumbersInBasket++;
  };

  @action
  decrease = () => {
    this.totalNumbersInBasket--;
  };
}
- کار این مخزن، تامین عدد جمع آیتم‌های انتخابی توسط کاربر است که در کامپوننت شمارشگر نمایش داده می‌شود.
- در اینجا خاصیت totalNumbersInBasket به صورت observable تعریف شده‌است و با تغییر آن چه به صورت مستقیم، با مقدار دهی آن و یا توسط دو action تعریف شده، سبب به روز رسانی UI خواهد شد.
- می‌شد این مخزن را با مخزن src\stores\market.js یکی کرد؛ اما جهت ارائه‌ی مثالی در مورد نحوه‌ی تعریف چند مخزن و روش برقراری ارتباط بین آن‌ها، به صورت مجزایی تعریف شد.

مخزن حالت src\stores\market.js
import { action, computed, observable } from "mobx";

export default class MarketStore {
  @observable basketItems = [];

  constructor(rootStore) {
    this.rootStore = rootStore;
  }

  @action
  add = product => {
    const selectedItem = this.basketItems.find(item => item.id === product.id);
    if (selectedItem) {
      selectedItem.count++;
    } else {
      this.basketItems.push({
        ...product,
        count: 1
      });
    }

    this.rootStore.counterStore.increase();
  };

  @action
  remove = id => {
    const selectedItem = this.basketItems.find(item => item.id === id);
    selectedItem.count--;

    if (selectedItem.count === 0) {
      this.basketItems.remove(selectedItem);
    }

    this.rootStore.counterStore.decrease();
  };

  @action
  removeAll = () => {
    this.basketItems = [];
    this.rootStore.counterStore.totalNumbersInBasket = 0;
  };

  @computed
  get totalPrice() {
    return this.basketItems.reduce((previous, current) => {
      return previous + current.price * current.count;
    }, 0);
  }
}
- کار این مخزن تامین مدیریت آرایه‌ی basketItems است که بیانگر اشیاء انتخابی توسط کاربر می‌باشد.
- توسط متد add آن در کامپوننت نمایش لیست محصولات، می‌توان آیتمی را به این آرایه اضافه کرد. در اینجا چون شیء product مورد استفاده دارای خاصیت count نیست، روش افزودن آن‌را توسط spread operator برای درج خواص شیء product اصلی و سپس تعریف آن‌را مشاهده می‌کنید. این فراخوانی، سبب افزایش یک واحد به عدد شمارشگر نیز می‌شود.
- متد remove آن در کامپوننت سبد خرید، مورد استفاده قرار می‌گیرد تا کاربر بتواند اطلاعاتی را از این لیست حذف کند. این فراخوانی، سبب کاهش یک واحد از عدد شمارشگر نیز می‌شود.
- متد removeAll آن در کامپوننت شمارشگر بالای صفحه استفاده می‌شود تا سبب خالی شدن آرایه‌ی آیتم‌های انتخابی گردد و همچنین عدد آن‌را نیز صفر کند.
- خاصیت محاسباتی totalPrice آن در پایین جدول سبد خرید، جمع کل هزینه‌ی قابل پرداخت را مشخص می‌کند.

مخزن حالت src\stores\index.js

در اینجا روش یکی کردن دو مخزن حالت یاد شده را به صورت خاصیت‌های عمومی یک مخزن کد ریشه، مشاهده می‌کنید:
import CounterStore from "./counter";
import MarketStore from "./market";

class RootStore {
  counterStore = new CounterStore(this);
  marketStore = new MarketStore(this);
}

export default RootStore;
هر مخزن مجزایی که تعریف شده، دارای یک پارامتر سازنده‌است که با مقدار شیء this کلاس RootStore مقدار دهی می‌شود. با این روش می‌توان بین مخازن کد مختلف ارتباط برقرار کرد. برای نمونه درمخزن حالت MarketStore، این پارامتر سازنده، امکان دسترسی به خاصیت counterStore و سپس تمام خاصیت‌ها و متدهای عمومی آن‌را فراهم می‌کند:
export default class MarketStore {
  @observable basketItems = [];

  constructor(rootStore) {
    this.rootStore = rootStore;
  }

  @action
  removeAll = () => {
    this.basketItems = [];
    this.rootStore.counterStore.totalNumbersInBasket = 0;
  };
}


تامین مخازن حالت تمام کامپوننت‌های برنامه

پس از ایجاد مخازن حالت، اکنون نیاز است آن‌ها را در اختیار سلسه مراتب کامپوننت‌های برنامه قرار دهیم. به همین جهت به فایل src\index.js مراجعه کرده و آن‌را به صورت زیر تغییر می‌دهیم:
import "./index.css";
import "bootstrap/dist/css/bootstrap.css";

import makeInspectable from "mobx-devtools-mst";
import { Provider } from "mobx-react";
import React from "react";
import ReactDOM from "react-dom";

import App from "./App";
import * as serviceWorker from "./serviceWorker";
import RootStore from "./stores";

const rootStore = new RootStore();

if (process.env.NODE_ENV === "development") {
  makeInspectable(rootStore); // https://github.com/mobxjs/mobx-devtools
}

ReactDOM.render(
  <Provider {...rootStore}>
    <App />
  </Provider>,
  document.getElementById("root")
);

serviceWorker.unregister();
- در اینجا ابتدا import فایل css بوت استرپ را مشاهده می‌کنید که در برنامه استفاده شده‌است.
- سپس یک وهله‌ی جدید از RootStore را که حاوی خاصیت‌های عمومی counterStore و marketStore است، ایجاد می‌کنیم.
- اگر علاقمند باشید تا حین کار با MobX، جزئیات پشت صحنه‌ی آن‌را توسط افزونه‌ی mobx-devtools ردیابی کنید، روش آن‌را در اینجا با فراخوانی متد makeInspectable مشاهده می‌کنید. مقدار process.env.NODE_ENV نیز بر اساس پروسه‌ی جاری node.js اجرا کننده‌ی برنامه‌ی React تامین می‌شود. اطلاعات بیشتر
- قسمت آخر این تنظیمات، محصور کردن کامپوننت App که بالاترین کامپوننت در سلسله مراتب کامپوننت‌های برنامه است، با شیء Provider می‌باشد. در این شیء توسط spread operator، سبب درج خواص عمومی rootStore، به عنوان مخازن قابل استفاده شده‌ایم. تنظیم {rootStore...} معادل عبارت زیر است:
<Provider counterStore={rootStore.counterStore} marketStore={rootStore.marketStore}>
به این ترتیب تمام کامپوننت‌های برنامه می‌توانند با دو مخزن کد ارسالی به آن‌ها کار کنند. در ادامه مشاهده می‌کنیم که چگونه این ویژگی‌ها، سبب تامین props کامپوننت‌ها خواهند شد.


اتصال کامپوننت ShopItemsList به مخزن حالت marketStore

پس از ایجاد rootStore و محصور کردن کامپوننت App توسط شیء Provider در فایل src\index.js، اکنون باید قسمت export default کامپوننت‌های برنامه را جهت استفاده‌ی از مخازن حالت، یکی یکی ویرایش کرد:
import { inject, observer } from "mobx-react";
import React from "react";

import products from "../services/productsService";

const ShopItemsList = ({ onAdd }) => {
  return (
  // ...
  );
};

export default inject(({ marketStore }) => ({
  onAdd: marketStore.add
}))(observer(ShopItemsList));
در اینجا فراخوانی متد inject، سبب دسترسی به ویژگی marketStore تامین شده‌ی توسط شیء Provider می‌شود. تمام ویژگی‌هایی که به شیء Provider ارائه می‌شوند، در اینجا به صورت خواصی که توسط Object Destructuring قابل استخراج هستند، قابل دسترسی می‌شوند. سپس props این کامپوننت را که متد onAdd را می‌پذیرد، از طریق marketStore.add تامین می‌کنیم. در آخر کامپوننت ShopItemsList باید به صورت یک observer بازگشت داده شود تا تغییرات store را تحت نظر قرار داده و به این صورت امکان به روز رسانی UI را پیدا کند.


اتصال کامپوننت BasketItemsList به مخزن حالت marketStore

در اینجا نیز سطر export default را جهت دریافت خاصیت marketStore، از شیء Provider تامین شده‌ی در فایل src\index.js، ویرایش می‌کنیم. به این ترتیب سه props مورد انتظار این کامپوننت، توسط خاصیت‌های basketItems (آرایه‌ی اشیاء انتخابی توسط کاربر)، totalPrice (خاصیت محاسباتی جمع کل هزینه) و  متد رویدادگردان onRemove (برای حذف یک آیتم) تامین می‌شوند. در آخر کامپوننت را به صورت observer محصور کرده و بازگشت می‌دهیم تا تغییرات در مخزن حالت آن، سبب به روز رسانی UI آن شوند:
import { inject, observer } from "mobx-react";
import React from "react";

const BasketItemsList = ({ items, totalPrice, onRemove }) => {
  return (
  // ...
  );
};

export default inject(({ marketStore }) => ({
  items: marketStore.basketItems,
  totalPrice: marketStore.totalPrice,
  onRemove: marketStore.remove
}))(observer(BasketItemsList));


اتصال کامپوننت BasketItemsCounter به دو مخزن حالت counterStore و marketStore

در اینجا روش استفاده‌ی از decorator syntax کتابخانه‌ی mobx-react را بر روی یک کامپوننت کلاسی مشاهده می‌کنید. تزئین کننده‌ی inject، امکان دسترسی به مخازن حالت تزریقی به شیء Provider را میسر کرده و سپس توسط آن می‌توان props مورد انتظار کامپوننت را از مخازن متناظر استخراج کرده و در اختیار کامپوننت قرار داد. همچنین این کامپوننت توسط تزئین کننده‌ی observer نیز علامت گذاری شده‌است. در این حالت نیازی به تغییر سطر export default نیست.
import { inject, observer } from "mobx-react";
import React, { Component } from "react";

@inject(rootStore => ({
  count: rootStore.counterStore.totalNumbersInBasket,
  onRemoveAll: rootStore.marketStore.removeAll
}))
@observer
class BasketItemsCounter extends Component {
  render() {
    const { count, onRemoveAll } = this.props;
    return (
      // ...
    );
  }
}

export default BasketItemsCounter;

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-with-mobx-part4.zip
اشتراک‌ها
کتابخانه ای جهت ساخت فایل RTL CSS

فریم ورک RTLCSS به شما این امکان را می‌دهد تا بجای نوشتن دو فایل CSS، تنها یک فایل برای نسخه LTR بنویسید. RTLCSS نسخه RTL را کامپایل می‌کند. 

کتابخانه ای جهت ساخت فایل RTL CSS
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت سیزدهم- فعالسازی اعتبارسنجی دو مرحله‌ای
در انتهای «قسمت دوازدهم- یکپارچه سازی با اکانت گوگل»، کار «اتصال کاربر وارد شده‌ی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است» انجام شد. اما این مورد یک مشکل امنیتی را هم ممکن است ایجاد کند. اگر IDP ثالث، ایمیل اشخاص را تعیین اعتبار نکند، هر شخصی می‌تواند ایمیل دیگری را بجای ایمیل اصلی خودش در آنجا ثبت کند. به این ترتیب یک مهاجم می‌تواند به سادگی تنها با تنظیم ایمیل کاربری مشخص و مورد استفاده‌ی در برنامه‌ی ما در آن IDP ثالث، با سطح دسترسی او فقط با دو کلیک ساده به سایت وارد شود. کلیک اول، کلیک بر روی دکمه‌ی external login در برنامه‌ی ما است و کلیک دوم، کلیک بر روی دکمه‌ی انتخاب اکانت، در آن اکانت لینک شده‌ی خارجی است.
برای بهبود این وضعیت می‌توان مرحله‌ی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شده‌ی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال می‌کنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامه‌ی ما نیز می‌باشد. این اعتبارسنجی دو مرحله‌ای را می‌توان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمه‌ی عبور در IDP ما نیز اضافه کرد.


تنظیم میان‌افزار Cookie Authentication

مرحله‌ی اول ایجاد گردش کاری اعتبارسنجی دو مرحله‌ای، فعالسازی میان‌افزار Cookie Authentication در برنامه‌ی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public const string TwoFactorAuthenticationScheme = "idsrv.2FA";

        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            services.AddAuthentication()
                .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme)
                .AddGoogle(authenticationScheme: "Google", configureOptions: options =>
                {
                    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                    options.ClientId = Configuration["Authentication:Google:ClientId"];
                    options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
                });
        }


اصلاح اکشن متد Login برای هدایت کاربر به صفحه‌ی ورود اطلاعات کد موقتی

تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیش‌فرض، کدهای HttpContext.SignInAsync آن‌را حذف کرده و با Redirect به اکشن متد نمایش صفحه‌ی ورود کد موقتی ارسال شده‌ی به آدرس ایمیل کاربر، جایگزین می‌کنیم.
namespace DNT.IDP.Controllers.Account
{
    [SecurityHeaders]
    [AllowAnonymous]
    public class AccountController : Controller
    {
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
    // ... 

            if (ModelState.IsValid)
            {
                if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password))
                {
                    var user = await _usersService.GetUserByUsernameAsync(model.Username);
                    
                    var id = new ClaimsIdentity();
                    id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId));
                    await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));

                    await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId);

                    var redirectToAdditionalFactorUrl =
                        Url.Action("AdditionalAuthenticationFactor",
                            new
                            {
                                returnUrl = model.ReturnUrl,
                                rememberLogin = model.RememberLogin
                            });                   

                    // request for a local page
                    if (Url.IsLocalUrl(model.ReturnUrl))
                    {
                        return Redirect(redirectToAdditionalFactorUrl);
                    }

                    if (string.IsNullOrEmpty(model.ReturnUrl))
                    {
                        return Redirect("~/");
                    }

                    // user might have clicked on a malicious link - should be logged
                    throw new Exception("invalid return URL");
                }

                await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
                ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
            }

            // something went wrong, show form with error
            var vm = await BuildLoginViewModelAsync(model);
            return View(vm);
        }
- در این اکشن متد، ابتدا مشخصات کاربر، از بانک اطلاعاتی بر اساس نام کاربری او، دریافت می‌شود.
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل می‌شود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شده‌ی در کلاس آغازین برنامه تطابق دارد، ایجاد می‌کنیم.
 await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
سپس کد موقتی به آدرس ایمیل کاربر ارسال می‌شود. برای این منظور سرویس جدید زیر را به برنامه اضافه کرده‌ایم:
    public interface ITwoFactorAuthenticationService
    {
        Task SendTemporaryCodeAsync(string subjectId);
        Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code);
    }
- کار متد SendTemporaryCodeAsync، ایجاد و ذخیره‌ی یک کد موقتی در بانک اطلاعاتی و سپس ارسال آن به کاربر است. البته در اینجا، این کد در صفحه‌ی Console برنامه لاگ می‌شود (یا هر نوع Log provider دیگری که برای برنامه تعریف کرده‌اید) که می‌توان بعدها آن‌را با کدهای ارسال ایمیل جایگزین کرد.
- متد IsValidTemporaryCodeAsync، کد دریافت شده‌ی از کاربر را با نمونه‌ی موجود در بانک اطلاعاتی مقایسه و اعتبار آن‌را اعلام می‌کند.


ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن

پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت می‌کنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهده‌ی لاگ برنامه)، دریافت کرده‌است، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال می‌کنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامه‌ی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations;

namespace DNT.IDP.Controllers.Account
{
    public class AdditionalAuthenticationFactorViewModel
    {
        [Required]
        public string Code { get; set; }

        public string ReturnUrl { get; set; }

        public bool RememberLogin { get; set; }
    }
}
که در آن، Code توسط کاربر تکمیل می‌شود و دو گزینه‌ی دیگر را از طریق مسیریابی و هدایت به این View دریافت خواهد کرد.
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش می‌دهد و در حالت Post، اطلاعات آن‌را از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account
{
    public class AccountController : Controller
    {
        [HttpGet]
        public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin)
        {
            // create VM
            var vm = new AdditionalAuthenticationFactorViewModel
            {
                RememberLogin = rememberLogin,
                ReturnUrl = returnUrl
            };

            return View(vm);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> AdditionalAuthenticationFactor(
            AdditionalAuthenticationFactorViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // read identity from the temporary cookie
            var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme);
            var tempUser = info?.Principal;
            if (tempUser == null)
            {
                throw new Exception("2FA error");
            }

            // ... check code for user
            if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code))
            {
                ModelState.AddModelError("code", "2FA code is invalid.");
                return View(model);
            }

            // login the user
            AuthenticationProperties props = null;
            if (AccountOptions.AllowRememberLogin && model.RememberLogin)
            {
                props = new AuthenticationProperties
                {
                    IsPersistent = true,
                    ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                };
            }

            // issue authentication cookie for user
            var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId());
            await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username));
            await HttpContext.SignInAsync(user.SubjectId, user.Username, props);

            // delete temporary cookie used for 2FA
            await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme);

            if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl))
            {
                return Redirect(model.ReturnUrl);
            }

            return Redirect("~/");
        }
توضیحات:
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد می‌کند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، می‌توانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شده‌است، آن‌را به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی می‌کنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت می‌کنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.


View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شده‌است تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel

<div>
    <div class="page-header">
        <h1>2-Factor Authentication</h1>
    </div>

    @Html.Partial("_ValidationSummary")

    <div class="row">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title">Input your 2FA code</h3>
            </div>
            <div class="panel-body">
                <form asp-route="Login">
                    <input type="hidden" asp-for="ReturnUrl" />
                    <input type="hidden" asp-for="RememberLogin" />

                    <fieldset>
                        <div class="form-group">
                            <label asp-for="Code"></label>
                            <input class="form-control" placeholder="Code" asp-for="Code" autofocus>
                        </div>

                        <div class="form-group">
                            <button class="btn btn-primary">Submit code</button>
                        </div>
                    </fieldset>
                </form>

            </div>
        </div>
    </div>
</div>

آزمایش برنامه جهت بررسی اعتبارسنجی دو مرحله‌ای

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


همچنین کد موقتی این مرحله را نیز در لاگ‌های برنامه مشاهده می‌کنید:


پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامه‌ی MVC Client هدایت می‌شویم.


اضافه کردن اعتبارسنجی دو مرحله‌ای به قسمت ورود از طریق تامین کننده‌های هویت خارجی

دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه می‌کنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شده‌است، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل می‌کنیم.




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 10 - بررسی تغییرات Viewها
تا اینجا یک پروژه‌ی خالی ASP.NET Core 1.0 را به مرحله‌ی فعال سازی ASP.NET MVC و تنظیمات مسیریابی‌های اولیه‌ی آن رسانده‌ایم. مرحله‌ی بعد، افزودن Viewها، نمایش اطلاعاتی به کاربران و دریافت اطلاعات از آن‌ها است و همانطور که پیشتر نیز عنوان شد، برای «ارتقاء» نیاز است «15 مورد» ابتدایی مطالب ASP.NET MVC سایت را پیش از ادامه‌ی این سری مطالعه کنید.

معرفی فایل جدید ViewImports

پروژه‌ی خالی ASP.NET Core 1.0 فاقد پوشه‌ی Views به همراه فایل‌های آغازین آن است. بنابراین ابتدا در ریشه‌ی پروژه، پوشه‌ی جدید Views را ایجاد کنید.
فایل‌های آغازین این پوشه هم در مقایسه‌ی با نگارش‌های قبلی ASP.NET MVC اندکی تغییر کرده‌اند. برای مثال در نگارش قبلی، فایل web.config ایی در ریشه‌ی پوشه‌ی Views قرار داشت و چندین مقصود را فراهم می‌کرد:
الف) در آن تنظیم شده بود که هر نوع درخواستی به فایل‌های موجود در پوشه‌ی Views، برگشت خورده و قابل پردازش نباشند. این مورد هم از لحاظ مسایل امنیتی اضافه شده بود و هم اینکه در ASP.NET MVC، برخلاف وب فرم‌ها، شروع پردازش یک درخواست، از فایل‌های View شروع نمی‌شود. به همین جهت است که درخواست مستقیم آن‌ها بی‌معنا است.
در ASP.NET Core، فایل web.config از این پوشه حذف شده‌است؛ چون دیگر نیازی به آن نیست. اگر مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک» را به خاطر داشته باشید، هر پوشه‌ای که توسط میان افزار Static Files عمومی نشود، توسط کاربران برنامه قابل دسترسی نخواهد بود و چون پوشه‌ی Views هم به صورت پیش فرض توسط این میان افزار عمومی نمی‌شود، نیازی به فایل web.config، جهت قطع دسترسی به فایل‌های موجود در آن وجود ندارد.

ب) کاربرد دیگر این فایل web.config، تعریف فضاهای نام پیش فرضی بود که در فایل‌های View مورد استفاده قرار می‌گرفتند. برای مثال چون فضای نام HTML Helperهای استاندارد ASP.NET MVC در این فایل web.config قید شده بود، دیگر نیازی به تکرار آن در تمام فایل‌های View برنامه وجود نداشت. در ASP.NET Core، برای جایگزین کردن این قابلیت، فایل جدیدی را به نام ViewImports.cshtml_ معرفی کرده‌اند، تا دیگر نیازی به ارث بری از فایل web.config وجود نداشته باشد.


برای مثال اگر می‌خواهید بالای Viewهای خود، مدام ذکر using مربوط به فضای نام مدل‌ها برنامه را انجام ندهید، این سطر تکراری را به فایل جدید view imports منتقل کنید:
 @using MyProject.Models

و این فضاهای نام به صورت پیش فرض برای تمام viewها مهیا هستند و نیازی به تعریف مجدد، ندارند:
• System
• System.Linq
• System.Collections.Generic
• Microsoft.AspNetCore.Mvc
• Microsoft.AspNetCore.Mvc.Rendering


افزودن یک View جدید

در نگارش‌های پیشین ASP.NET MVC، اگر بر روی نام یک اکشن متد کلیک راست می‌کردیم، در منوی ظاهر شده، گزینه‌ی Add view وجود داشت. چنین گزینه‌ای در نگارش RTM اول ASP.NET Core وجود ندارد و مراحل ایجاد یک View جدید را باید دستی طی کنید. برای مثال اگر نام کلاس کنترلر شما PersonController است، پوشه‌ی Person را به عنوان زیر پوشه‌ی Views ایجاد کرده و سپس بر روی این پوشه کلیک راست کنید، گزینه‌ی add new item را انتخاب و سپس واژه‌ی view را جستجو کنید:


البته یک دلیل این مساله می‌تواند امکان سفارشی سازی محل قرارگیری این پوشه‌ها در ASP.NET Core نیز باشد که در ادامه آن‌را بررسی خواهیم کرد (و ابزارهای از پیش تعریف شده عموما با مکان‌های از پیش تعریف شده کار می‌کنند).


امکان پوشه بندی بهتر فایل‌های یک پروژه‌ی ASP.NET Core نسبت به مفهوم Areas در نگارش‌های پیشین ASP.NET MVC

حالت پیش فرض پوشه بندی فایل‌های اصلی برنامه‌های ASP.NET MVC، مبتنی بر فناوری‌ها است؛ برای مثال پوشه‌های views و Controllers و امثال آن تعریف شده‌اند.
Project   
- Controllers
- Models
- Services
- ViewModels
- Views
روش دیگری را که برای پوشه بندی پروژه‌های ASP.NET MVC پیشنهاد می‌کنند (که Area توکار آن نیز زیر مجموعه‌ی آن محسوب می‌شود)، اصطلاحا Feature Folder Structure نام دارد. در این حالت برنامه بر اساس ویژگی‌ها و قابلیت‌های مختلف آن پوشه بندی می‌شود؛ بجای اینکه یک پوشه‌ی کلی کنترلرها را داشته باشیم و یک پوشه‌ی کلی views را که پس از مدتی، ارتباط دادن بین این‌ها واقعا مشکل می‌شود.
هرکسی که مدتی با ASP.NET MVC کار کرده باشد حتما به این مشکل برخورده‌است. درحال پیاده سازی قابلیتی هستید و برای اینکار نیاز خواهید داشت مدام بین پوشه‌های مختلف برنامه سوئیچ کنید؛ از پوشه‌ی کنترلرها به پوشه‌ی ویووها، به پوشه‌ی اسکریپت‌ها، پوشه‌ی اشتراکی ویووها و غیره. پس از رشد برنامه به جایی خواهید رسید که دیگر نمی‌توانید تشخیص دهید این فایلی که اضافه شده‌است ارتباطش با سایر قسمت‌ها چیست؟
ایده‌ی «پوشه بندی بر اساس ویژگی‌ها»، بر مبنای قرار دادن تمام نیازهای یک ویژگی، درون یک پوشه‌ی خاص آن است:


همانطور که مشاهده می‌کنید، در این حالت تمام اجزای یک ویژگی، داخل یک پوشه قرار گرفته‌اند؛ از کنترلر مرتبط با Viewهای آن تا فایل‌های css و js خاص آن.
برای پیاده سازی آن:
1) نام پوشه‌ی Views را به Features تغییر دهید.
2) پوشه‌ای را به نام StartupCustomizations به برنامه اضافه کرده و سپس کلاس ذیل را به آن اضافه کنید:
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor;
 
namespace Core1RtmEmptyTest.StartupCustomizations
{
  public class FeatureLocationExpander : IViewLocationExpander
  {
   public void PopulateValues(ViewLocationExpanderContext context)
   {
    context.Values["customviewlocation"] = nameof(FeatureLocationExpander);
   }
 
   public IEnumerable<string> ExpandViewLocations(
    ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
   {
    return new[]
    {
      "/Features/{1}/{0}.cshtml",
      "/Features/Shared/{0}.cshtml"
    };
   }
  }
}
حالت پیش فرض ASP.NET MVC، یافتن فایل‌ها در مسیرهای Views/{1}/{0}.cshtml و Views/Shared/{0}.cshtml است؛ که در اینجا {0} نام view است و {1} نام کنترلر. این ساختار هم در اینجا حفظ شده‌است؛ اما اینبار به پوشه‌ی جدید Features اشاره می‌کند.
RazorViewEngine برنامه، بر اساس وهله‌ی پیش فرضی از اینترفیس IViewLocationExpander، محل یافتن Viewها را دریافت می‌کند. با استفاده از پیاه سازی فوق، این پیش فرض‌ها را به پوشه‌ی features هدایت کرده‌ایم.
3) در ادامه به کلاس آغازین برنامه مراجعه کرده و پس از فعال سازی ASP.NET MVC، این قابلیت را فعال سازی می‌کنیم:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
  services.Configure<RazorViewEngineOptions>(options =>
  {
   options.ViewLocationExpanders.Add(new FeatureLocationExpander());
  });
4) اکنون تمام فایل‌های مرتبط با یک ویژگی را به پوشه‌ی خاص آن انتقال دهید. منظور از این فایل‌ها، کنترلر، فایل‌های مدل و ویوومدل، فایل‌های ویوو و فایل‌های js و css هستند و نه مورد دیگری.
5) اکنون باید پوشه‌ی Controllers خالی شده باشد. این پوشه را کلا حذف کنید. از این جهت که کنترلرها بر اساس پیش فرض‌های ASP.NET MVC (کلاس ختم شده‌ی به کلمه‌ی Controller واقع در اسمبلی که از وابستگی‌های ASP.NET MVC استفاده می‌کند) در هر مکانی که تعریف شده باشند، یافت خواهند شد و پوشه‌ی واقع شدن آن‌ها مهم نیست.
6) در آخر به فایل project.json مراجعه کرده و قسمت publish آن‌را جهت درج نام پوشه‌ی Features اصلاح کنید (تا در حین توزیع نهایی استفاده شود):
"publishOptions": {
 "include": [
  "wwwroot",
  "Features",
  "appsettings.json",
  "web.config"
 ]
},


در اینجا نیز یک نمونه‌ی دیگر استفاده‌ی از این روش بسیار معروف را مشاهده می‌کنید.


امکان ارائه‌ی برنامه بدون ارائه‌ی فایل‌های View آن

ASP.NET Core به همراه یک EmbeddedFileProvider نیز هست. حالت پیش فرض آن PhysicalFileProvider است که بر اساس تنظیمات IViewLocationExpander توکار (و یا نمونه‌ی سفارشی فوق در مبحث پوشه‌ی ویژگی‌ها) کار می‌کند.
برای راه اندازی آن ابتدا نیاز است بسته‌ی نیوگت ذیل را به فایل project.json اضافه کرد:
{
  "dependencies": {
        //same as before
   "Microsoft.Extensions.FileProviders.Embedded": "1.0.0"
  },
سپس تنظیمات متد ConfigureServices کلاس آغازین برنامه را به صورت ذیل جهت معرفی EmbeddedFileProvider تغییر می‌دهیم:
services.AddMvc();
services.Configure<RazorViewEngineOptions>(options =>
{
  options.ViewLocationExpanders.Add(new FeatureLocationExpander());
 
  var thisAssembly = typeof(Startup).GetTypeInfo().Assembly; 
  options.FileProviders.Clear();
  options.FileProviders.Add(new EmbeddedFileProvider(thisAssembly, baseNamespace: "Core1RtmEmptyTest"));
});
و در آخر در فایل project.json، در قسمت build options، گزینه‌ی embed را مقدار دهی می‌کنیم:
"buildOptions": {
  "emitEntryPoint": true,
  "preserveCompilationContext": true,
  "embed": "Features/**/*.cshtml"
},
در اینجا چند نکته را باید مدنظر داشت:
1) اگر نام پوشه‌ی Views را به Features تغییر داده‌اید، نیاز به ثبت ViewLocationExpanders آن‌را دارید (وگرنه، خیر).
2) در اینجا جهت مثال و بررسی اینکه واقعا این فایل‌ها از اسمبلی برنامه خوانده می‌شوند، متد options.FileProviders.Clear فراخوانی شده‌است. این متد PhysicalFileProvider  پیش فرض را حذف می‌کند. کار PhysicalFileProvider  خواندن فایل‌های ویووها از فایل سیستم به صورت متداول است.
3) کار قسمت embed در تنظیمات build، افزودن فایل‌های cshtml به قسمت منابع اسمبلی است (به همین جهت دیگر نیازی به توزیع آن‌ها نخواهد بود). اگر صرفا **/Features را ذکر کنید، تمام فایل‌های موجود را پیوست می‌کند. همچنین اگر نام پوشه‌ی Views را تغییر نداده‌اید، این مقدار همان Views/**/*.cshtml خواهد بود و یا **/Views


4) در EmbeddedFileProvider می‌توان هر نوع اسمبلی را ذکر کرد. یعنی می‌توان برنامه را به صورت چندین و چند ماژول تهیه و سپس سرهم و یکپارچه کرد (options.FileProviders یک لیست قابل تکمیل است). در اینجا ذکر baseNamespace نیز مهم است. در غیر اینصورت منبع مورد نظر از اسمبلی یاد شده، قابل استخراج نخواهد بود (چون نام اسمبلی، قسمت اول نام هر منبعی است).


فعال سازی کامپایل خودکار فایل‌های View در ASP.NET Core 1.0

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