مطالب
آشنایی با M.A.F - قسمت اول

در طی چند مقاله قصد بررسی نحوه‌ی تولید برنامه‌های توسعه پذیر (extensible) را با استفاده از plug-ins و یا add-ins داریم.

افزونه‌ها عموما در سه گروه قرار می‌گیرند:
الف) افزونه، سرویسی را به هاست ارائه می‌دهد. برای مثال یک میل سرور نیاز به افزونه‌هایی برای ویروس یابی یا فیلتر کردن هرزنامه‌ها دارد؛ یا یک برنامه پردازش متنی نیاز به افزونه‌ای جهت بررسی غلط‌های املایی می‌تواند داشته باشد و یا یک مرورگر وب می‌تواند با کمک افزونه‌ها قابلیت‌های پیش فرض خود را به شدت توسعه و افزایش دهد (نمونه‌ی بارز آن فایرکس است که عمده‌ترین دلیل اقبال عمومی به آن سهولت توسعه پذیری آن می‌باشد).
ب) در گروه دوم، هاست، رفتار مشخصی را ارائه داده و سپس افزونه بر اساس آن، نحوه‌ی عملکرد هاست را مشخص می‌کند. در این حالت هاست است که سرویسی را به افزونه ارائه می‌دهد. نمونه‌ی بازر آن افزونه‌های آفیس هستند که امکان اتوماسیون فرآیندهای مختلف آن‌را میسر می‌سازند. به این صورت امکان توسعه‌ی یک برنامه به شکلی که در طراحی اولیه آن اصلا انتظار آن نمی‌رفته وجود خواهد داشت. همچنین در اینجا نیازی به داشتن سورس کد برنامه‌ی اصلی نیز نمی‌باشد.
ج) گروه سوم افزونه‌ها تنها از هاست جهت نمایش خود استفاده کرده و عملا استفاده‌ی خاصی از هاست ندارد. برای مثال نوار ابزاری که خود را به windows explorer متصل می‌کند و تنها از آن جهت نمایش خود بهره می‌جوید.


در حال حاضر حداقل دو فریم ورک عمده جهت انجام این‌کار و تولید افزونه‌ها برای دات نت فریم ورک مهیا است:
الف) managed addin framework یا MAF
ب) managed extensibility framework یا MEF

فضای نام جدیدی به دات نت فریم ورک سه و نیم به نام System.AddIn اضافه شده است که به آن Managed AddIn Framework یا MAF نیز اطلاق می‌شود. از این فریم ورک در VSTO (تولید افزونه برای مجموعه‌ی آفیس) توسط خود مایکروسافت استفاده شده است.

فریم ورک توسعه‌ی افزونه‌های مدیریت شده در دات نت فریم ورک سه و نیم، مزایای زیر را در اختیار ما خواهد گذاشت:
- امکانات load و unload افزونه‌های تولید شده
- امکان تغییر افزونه‌ها در زمان اجرای برنامه اصلی بدون نیاز به بستن آن
- ارائه‌ی محیطی ایزوله با ترسیم مرزی بین افزونه و برنامه اصلی
- مدیریت طول عمر افزونه
- مدیریت سازگاری با نگارش‌های قبلی و یا بعدی یک افزونه
- امکانات به اشتراک گذاری افزونه‌ها با برنامه‌های دیگر
- تنظیمات امنیتی و مشخص سازی سطح دسترسی افزونه‌ها
و ...

یک راه حل مبتنی بر MAF می‌تواند شامل 7 پروژه باشد (که به روابط تعریف شده در آن pipeline هم گفته می‌شود):

Host : همان برنامه‌ی اصلی است که توسط یک سری افزونه، توسعه یافته است.
Host View : بیانگر انتظارات هاست از افزونه‌ها است. به عبارت دیگر افزونه‌ها باید موارد لیست شده در این پروژه را پیاده سازی کنند.
Host Side Adapter : پل ارتباطی Host View و پروژه‌ی Contract است.
Contract: اینترفیسی است که کار برقراری ارتباط بین Host و افزونه‌ها را برعهده دارد.
Add-In Side Adapter : پل ارتباطی بین Add-In View و Contract است.
Add-In View :‌ حاوی متدها و اشیایی است که جهت برقراری ارتباط با هاست از آن‌ها استفاده می‌شود.
Add-In : اسمبلی است که توسط هاست جهت توسعه‌ی قابلیت‌های خود بارگذاری می‌شود (به آن Add-On ، Extension ، Plug-In و Snap-In هم گفته می‌شود).

هدف از این جدا سازی‌ها ارائه‌ی راه حل loosely-coupledایی است که امکان ایزوله سازی، اعمال شرایط امنیتی ویژه و همچنین کنترل نگارش‌های مختلف را تسهیل می‌بخشد و این امر با استفاده از interface های معرفی شده میسر گردیده است. این pipeline از قسمت‌های ذیل تشکیل می‌شود:



قرار داد یا Contract
برای تولید یک افزونه نیاز است تا بین هاست و افزونه قراردادی بسته شود. با توجه به استفاده از MAF ، روش تعریف این قرار داد برای مثال در یک افزونه‌ی مترجم به صورت زیر باید باشد:

[AddInContract]
public interface ITranslator : IContract
{
string Translate(string input);
}

استفاده از ویژگی AddInContract و پیاده سازی اینترفیس IContract جزو مراحل کاری استفاده از MAF است. MAF هنگام تولید پویای pipeline ذکر شده به دنبال ویژگی AddInContract می‌گردد. این موارد در فضای نام System.AddIn.Pipeline تعریف شده‌اند.

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

دیدگاه هاست نسبت به قرار داد:
public abstract class TranslatorHostView
{
public abstract string Translate(string input);
}
دیدگاه افزونه نسبت به قرار داد:
[AddInBase]
public abstract class TranslatorHostView
{
public abstract string Translate(string input);
}

هر دو کلاس فوق بر اساس قرار موجود بنا می‌شوند اما وابسته به آن نیستند. به همین جهت به صورت کلاس‌هایی abstract تعریف شده‌اند. در سمت افزونه، کلاس تعریف شده دیدگاه آن با کلاس دیدگاه سمت هاست تقریبا یکسان می‌باشد؛ اما با ویژگی AddInBase تعریف شده در فضای نام System.AddIn.Pipeline مزین گردیده است.


وفق دهنده‌ها یا Adapters
آخرین قسمت pipeline ، وفق دهنده‌ها هستند که کار آن‌ها اتصال قرار داد به دیدگاه‌ها است و توسط آن مدیریت طول عمر افزونه و همچنین تبدیل اطلاعات بین قسمت‌های مختلف انجام می‌شود. شاید در نگاه اول وجود آن‌ها زائد به نظر برسد اما این جدا سازی کدها سبب تولید افزونه‌هایی خواهد شد که به نگارش هاست و برنامه اصلی وابسته نبوده و بر عکس (version tolerance). به دو کلاس زیر دقت نمائید:

کلاس زیر با ویژگی [HostAdapter] تعریف شده در فضای نام System.AddIn.Pipeline، مزین شده است و کار آن اتصال HostView به Contract می‌باشد. برای این منظور TranslatorHostView ایی را که پیشتر معرفی کردیم باید پیاده سازی نماید. علاوه بر این با ایجاد وهله‌ای از کلاس ContractHandle ، کار مدیریت طول عمر افزونه را نیز می‌توان انجام داد.

[HostAdapter]
public class TranslatorHostViewToContract : TranslatorHostView
{
ITranslator _contract;
ContractHandle _lifetime;

public TranslatorHostViewToContract(ITranslator contract)
{
_contract = contract;
_lifetime = new ContractHandle(contract);
}

public override string Translate (string inp)
{
return _contract.Translate(inp);
}
}
کلاس سمت افزونه نیز بسیار شبیه قسمت قبل است و کار آن اتصال AddInView به Contract می‌باشد که با پیاده سازی ContractBase و Itranslator صورت خواهد گرفت. همچنین این کلاس به ویژگی AddInAdapter مزین گردیده است.

[AddInAdapter]
public class TranslatorAddInViewToContract : ContractBase, ITranslator
{
TranslatorAddInView _view;

public TranslatorAddInViewToContract(TranslatorView view)
{
_view = view;
}

public string Translate(string inp)
{
return _view.Translate(inp);
}
}

قسمت عمده‌ای از این کدها تکراری است. جهت سهولت تولید این کلاس‌ها و پروژه‌های مرتبط، تیم مربوطه برنامه‌ای را به نام pipeline builder ارائه داده است که از آدرس زیر قابل دریافت است:


این برنامه با دریافت اسمبلی مربوط بهcontract ، کار ساخت خودکار کلاس‌های adapters و views را انجام خواهد داد.

ایجاد افزونه
پس از ساخت قسمت‌های مختلف pipeline ، اکنون می‌توان افزونه را ایجاد نمود. هر افزونه باید add-in view را پیاده سازی کرده و با ویژگی AddIn مزین شود. برای مثال:

[AddIn("GoogleTranslator", Description="Universal translator",
Version="1.0.0.0", Publisher="YourName")]
public class GoogleAddIn : TranslatorAddInView
{
public string Translate(string input)
{
...
}
}

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

مطالب
پیدا کردن وابستگی‌های اشیاء در SQL Server

با بالا رفتن تعداد اشیاء تعریف شده در SQL server ، نگهداری آنها نیز مشکل‌تر می‌شود. در این حالت تغییر کوچکی در یکی از اشیاء ممکن است باعث از کار افتادن قسمتی از سیستم شود. بنابراین قبل از هر گونه تغییری در یک شیء، ابتدا باید سایر اشیاء وابسته به آن‌ را یافت و در نظر داشت ( dependencies ). برای این منظور ( impact analysis ) راه‌کارهای مختلفی در SQL server وجود دارد که در ادامه به آن‌ها خواهیم پرداخت:

الف) استفاده از امکانات management studio (اس کیوال سرور 2005 به بعد)

ساده‌ترین راه ممکن که گزارش مفصلی را نیز ارائه می‌دهد، کلیک بر روی یک شیء در management studio و انتخاب گزینه view dependencies است (شکل زیر).


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

ب) کوئری گرفتن از جداول سیستمی

امکانات قسمت قبل را با استفاده از اطلاعات جدول syscomments نیز می‌توان شبیه سازی کرد. در این جدول اطلاعات تعاریف کلیه view ، trigger ، رویه‌های ذخیره شده و غیره نگهداری می‌شود. برای مثال فرض کنید قصد داریم در جدول Orders دیتابیس Northwind ، نام فیلد OrderDate را تغییر دهیم. قبل از این‌کار بهتر است کوئری زیر را اجرا کنیم تا نام اشیاء وابسته را بدست آوریم:
SELECT NAME
FROM syscomments c
JOIN sysobjects o
ON c.id = o.id
WHERE TEXT LIKE '%OrderDate%'
AND TEXT LIKE '%Orders%'


این روش انعطاف پذیری بیشتری را نسبت به امکانات قسمت الف ، ارائه می‌دهد. برای نمونه فرض کنید می‌خواهید در یک دیتابیس کلیه اشیایی که عملیات delete را انجام می‌دهند پیدا کنید (جستجوی اشیاء حاوی یک عبارت خاص). در این مورد خواهیم داشت:

SELECT NAME
FROM syscomments c
JOIN Northwind.dbo.sysobjects o
ON c.id = o.id
WHERE TEXT LIKE '%delete%'

ج) استفاده از رویه ذخیره شده سیستمی sp_depends

جدول سیستمی دیگری در اس کیوال سرور به نام sysdepends وجود دارد که اطلاعات وابستگی‌های اشیاء در آن‌ها نگهداری می‌شود. برای دسترسی به اطلاعات این جدول ، اس کیوال سرور رویه ذخیره شده سیستمی sp_depends را ارائه داده است. برای مثال فرض کنید می‌خواهیم لیست اشیایی را که به جدول Oredres دیتابیس Northwind وابسته هستند، پیدا کنیم. در این حالت داریم:
USE Northwind
EXEC sp_depends 'Orders'


د) استفاده از schema view

با استفاده از view سیستمی INFORMATION_SCHEMA.ROUTINES ، که از ترکیب جداول syscolumns و sysobjects ایجاد شده است نیز می‌توان عملکرد sp_depends را شبیه سازی کرد اما جداول و view ها از گزارش آن حذف شده‌اند.
SELECT routine_name,
routine_type
FROM INFORMATION_SCHEMA.ROUTINES
WHERE routine_definition LIKE '%Orders%'

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



ه) استفاده از برنامه SQL Dependency Tracker

نسخه آزمایشی برنامه ذکر شده را از این آدرس می‌توان دریافت کرد.


همانطور که ملاحظه می‌کنید این جستجوها بر روی اطلاعات ذخیره شده در اس کیوال سرور صورت می‌گیرند و اگر در کدهای خود در خارج از اس کیوال سرور مخلوطی از عبارات اس کیوال را داشته باشید، نگهداری آنها بسیار مشکل خواهد بود. بنابراین تا حد ممکن باید عملیات مرتبط را در دیتابیس و توسط اشیاء اس کیوال سرور مانند رویه‌های ذخیره شده، view ها و امثال آن‌ها انجام داد تا این جدا سازی به‌خوبی صورت گرفته و در زمان نیاز به انجام تغییرات، ردگیری اشیاء وابسته به‌سادگی صورت گیرد.


مطالب
پردازش فایل‌های XML با استفاده از jQuery

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

و در حالت کلی :
http://api.feedburner.com/awareness/1.0/GetFeedData?uri=<feeduri>

که حاصل آن برای مثال یک فایل XML با فرمت زیر می‌باشد:

<rsp stat="ok">
<feed id="fhphjt61bueu08k93ehujpu234" uri="vahidnasiri">
<entry date="2009-01-23" circulation="153" hits="276" reach="10"/>
</feed>
</rsp>

همانطور که مطلع هستید چند روزی است که jQuery 1.3.1 ارائه شده است. جهت استفاده از آخرین نگارش موجود آن تا این زمان، می‌توان از گوگل به عنوان هاست این کتابخانه به صورت زیر استفاده کرد:

<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js' type='text/javascript'></script>

نحوه خواندن مقدار circulation فایل xml ذخیره شده بر روی کامپیوتر:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>FeedBurner API</title>
<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js' type='text/javascript'>
</script>
<script type="text/javascript">
function parseXml(xml){
//find every entry and print the circulation
$(xml).find("entry").each(function(){
$("#output").append($(this).attr("circulation"));
});
}

$(document).ready(function(){
$.ajax({
type: "GET",
url: "GetFeedData_local.xml",
dataType: "xml",
success: parseXml
});
});
</script>
</head>
<body>
<div dir="rtl" style="font-family:tahoma; font-size:12px;">
تعداد مشترکین تغذیه خبری سایت:
<div id="output">
</div>
</div>
</body>
</html>

با استفاده از قابلیت Ajax کتابخانه jQuery ، اطلاعات فایل محلی GetFeedData_local.xml دریافت شده و محتوای آن به تابع parseXml پاس می‌شود (توسط قسمت success). سپس در این تابع تمام تگ‌های entry یافت شده و مقدار circulation آن‌ها به یک div با ID معادل output اضافه می‌شود.
مثال فوق در مورد خواندن اطلاعات از یک فایل xml می‌تواند برای مثال این کاربرد را در یک سایت داشته باشد:
نمایش اتفاقی سخن روز یا سخن بزرگان و امثال آن بدون برنامه نویسی سمت سرور جهت انجام این کار از یک فایل xml تهیه شده، بدون نیاز به استفاده از دیتابیس خاصی.

تا اینجای کار مشکلی نیست. اما همانطور که در مطلب مقابله با حملات CSRF نیز ذکر شد، مرورگرهای جدید امکان ارسال یا دریافت اطلاعات به صورت Ajax را بین سایت‌ها ممنوع کرده‌اند (ماجرا هم از آنجا شروع شد که یکبار جی‌میل این باگ امنیتی را داشته است). بنابراین اگر شما بجای url قسمت Ajax فوق، آدرس سایت فید برنر را قرار دهید با خطای زیر متوقف خواهید شد:

Access to restricted URI denied

تمام موارد دیگری هم که در jQuery برای دریافت اطلاعات از یک فایل یا url موجود است (مثلا تابع load یا get و امثال آن) فقط به سایت جاری و دومین جاری باید ختم شوند در غیر اینصورت توسط مرورگرهای جدید متوقف خواهند شد.

مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت پنجم - Redux Hooks
تا اینجا الگوی Redux را در برنامه‌های React بررسی کردیم که شامل این موارد است:
- با استفاده از Redux، یک شیء سراسری state، کار مدیریت state تمام برنامه را به عهده می‌گیرد که به آن «single source of truth» نیز گفته می‌شود. البته هرچند می‌توان کامپوننت‌هایی را هم در این بین داشت که state خاص خودشان را داشته باشند و آن‌را در این شیء سراسری ذخیره نکنند.
- در حین کار با Redux، تنها راه تغییر شیء سراسری state آن، صدور رخ‌دادهایی هستند که در اینجا اکشن نامیده می‌شوند. یک اکشن شیءای است که بیان می‌کند چه چیزی قرار است تغییر کند.
- برای ساده سازی ساخت این اشیاء می‌توان متدهایی را به نام action creators ایجاد کرد.
- اگر این متدهای action creator را توسط متد store.dispatch فراخوانی کنیم، سبب dispatch شیء اکشن، به یک تابع Reducer متناظری خواهند شد. این تابع Reducer است که قسمتی از state را که متناظر با نوع اکشن رسیده‌است، تغییر می‌دهد. در این حالت اگر اکشن رسیده، نوع مدنظری را نداشته باشد، خروجی تابع Reducer، همان state اصلی و بدون تغییر خواهد بود.
- Reducerها توابعی خالص هستند و نباید به همراه اثرات جانبی باشند (هر نوع تعاملی با دنیای خارج از تابع جاری) و همچنین نباید شیء state را نیز مستقیما تغییر دهند. این توابع باید یک کپی تغییر یافته‌ی از state را در صورت نیاز بازگشت دهند.
- برای مدیریت بهتر برنامه می‌توان چندین تابع Reducer را بر اساس نوع‌های اکشن‌های ویژه‌ای، پیاده سازی کرد. سپس با ترکیب آن‌ها، یک شیء rootReducer ایجاد می‌شود.
- در نهایت در الگوی Redux، یک مخزن یا store تعریف خواهد شد که تمام این اجزاء را مانند rootReducer و میان‌افزارهای تعریف شده مانند Thunk، در کنار هم قرار می‌دهد و امکان dispatch اکشن‌ها را میسر می‌کند.
- اکنون برای استفاده‌ی از Redux در یک برنامه‌ی React، نیاز است کامپوننت ریشه‌ی برنامه را توسط کامپوننت Provider آن محصور کرد تا قسمت‌های مختلف برنامه بتوانند با امکانات مخزن Redux، کار کرده و با آن ارتباط برقرار کنند.
- قسمت آخر این اتصال جائی است که کامپوننت‌های اصلی برنامه، توسط یک کامپوننت دربرگیرنده که Container نامیده می‌شود، توسط متد connect کتابخانه‌ی react-redux محصور می‌شوند. به این ترتیب این کامپوننت‌ها می‌توانند state و خواص مورد نیاز خود را از طریق props دریافت کرده (mapStateToProps) و یا رویدادها را به سمت store، ارسال کنند (mapDispatchToProps).

از زمان React 16.8، مفهوم جدیدی به نام React Hooks معرفی شد که تعدادی از مهم‌ترین‌های آن‌ها را در سری «React 16x» بررسی کردیم. توسط Hooks، کامپوننت‌های تابعی React اکنون می‌توانند به local state خود دسترسی پیدا کنند و یا با دنیای خارج ارتباط برقرار کنند. پس از آن سایر کتابخانه‌های نوشته شده‌ی برای React نیز شروع به انطباق خود با این الگوی جدید کرده‌اند؛ برای مثال کتابخانه‌ی react-redux v1.7 نیز به همراه تعدادی Hook، جهت ساده سازی آخرین قسمتی است که در اینجا بیان شد، تا بتوانند راه حل دومی برای اتصال کامپوننت‌ها و دربرگیری آن‌ها باشند که در ادامه جزئیات آن‌ها را بررسی خواهیم کرد.


بررسی useSelector Hook

useSelector Hook که توسط کتابخانه‌ی react-redux ارائه می‌شود، معادل بسیار نزدیک تابع mapStateToProps مورد استفاده‌ی در متد connect است. برای مثال در قسمت قبل، دربرگیرنده‌ی کامپوننت Posts در فایل src\containers\Posts.js، یک چنین محتوایی را دارد:
import { connect } from "react-redux";

import Posts from "../components/Posts";

const mapStateToProps = state => {
  console.log("PostsContainer->mapStateToProps", state);
  return {
    ...state.postsReducer
  };
};

export default connect(mapStateToProps)(Posts);
اینبار اگر بخواهیم کل این container را حذف کرده و از useSelector Hook استفاده کنیم، به این ترتیب عمل خواهیم کرد:
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت می‌کرد:
const Posts = ({ posts, loading, error }) => {
  return (
  // ...
و این سه خاصیت دقیقا از متد mapStateToProps فوق که ملاحظه می‌کنید، تامین می‌شود. این متد خواص شیء state.postsReducer را به صورت props به کامپوننت Posts از طریق متد connect، ارسال می‌کند. کار postsReducer، فراهم آوردن و مدیریت سه خاصیت { loading: false, posts: [], error: null } است.

اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر می‌دهیم:
import { useSelector } from "react-redux";

// ...

const HooksPosts = () => {
  const { posts, loading, error } = useSelector(state => state.postsReducer);
  return (
  // ...
متد useSelector، امکان دسترسی به state ذخیره شده‌ی در مخزن redux را میسر می‌کند. سپس باید همانند متد mapStateToProps، خواصی را که از آن نیاز داریم، دریافت کنیم که در اینجا کل خواص postsReducer دریافت شده (کل state دریافت شده و سپس خاصیت state.postsReducer آن بازگشت داده شده‌است) و در ادامه توسط Object Destructuring، به سه متغیری که پیشتر از طریق props تامین می‌شدند، انتساب داده می‌شود.

یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز می‌توان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.

این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، می‌توان دربرگیرنده‌ی کامپوننت Posts را:
import PostsContainer from "./containers/Posts";

function App() {
  return (
    <main className="container">
      <PostsContainer />
    </main>
  );
}
با کامپوننت جدید HooksPosts جایگزین کرد و دیگر نیازی به نوشتن متد connect و ساخت یک container مخصوص آن، نیست:
import HooksPosts from "./components/HooksPosts";

function App() {
  return (
    <main className="container">
      <HooksPosts />
    </main>
  );
}


بررسی useDispatch Hook

تا اینجا موفق شدیم متد mapStateToProps را با useSelector Hook جایگزین کنیم. مرحله‌ی بعد، جایگزین کردن mapDispatchToProps با هوک دیگری به نام useDispatch است. برای مثال در قسمت قبل، دربرگیرنده‌ی کامپوننت FetchPosts در فایل src\containers\FetchPosts.js، چنین تعریفی را دارد:
import { connect } from "react-redux";

import { fetchPostsAsync } from "../actions";
import FetchPosts from "../components/FetchPosts";

const mapDispatchToProps = {
  fetchPostsAsync
};

export default connect(null, mapDispatchToProps)(FetchPosts);
کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است که در فایل src\components\FetchPosts.jsx به این صورت تعریف شده‌است:
const FetchPosts = ({ fetchPostsAsync }) => {
اکنون برای جایگزین کردن mapDispatchToProps با useDispatch Hook، نگارش دیگری از این کامپوننت تابعی را به نام HooksFetchPosts در فایل src\components\HooksFetchPosts.jsx ایجاد می‌کنیم:
import React from "react";
import { useDispatch } from "react-redux";

import { fetchPostsAsync } from "../actions";

const HooksFetchPosts = () => {
  const dispatch = useDispatch();
  return (
    <section className="card mt-5">
      <div className="card-header text-center">
        <button
          className="btn btn-primary"
          onClick={() => dispatch(fetchPostsAsync())}
        >
          Fetch Posts
        </button>
      </div>
    </section>
  );
};

export default HooksFetchPosts;
عملکر آن نیز بسیار ساده‌است. متد useDispatch، به ما امکان دسترسی به متد store.dispatch را می‌دهد (ارجاعی به آن‌را در اختیار ما قرار می‌دهد). اکنون اگر مانند رخ‌داد onClick تعریف شده، سبب dispatch یک action creator به نام fetchPostsAsync شویم (که اینبار باید به صورت صریح از ماژول مربوطه import شود؛ چون دیگر از طریق props تامین نمی‌شود)، سبب ارسال نتیجه‌ی آن به reducer متناظری می‌شود.

با این تغییر نیز می‌توان به فایل src\App.js مراجعه کرد و المان قبلی FetchPostsContainer را که از ماژول containers/FetchPosts تامین می‌شد، به نحو متداولی با همان کامپوننت جدید HooksFetchPosts، تعویض کرد:
import HooksFetchPosts from "./components/HooksFetchPosts";
import HooksPosts from "./components/HooksPosts";

// ...

function App() {
  return (
    <main className="container">
      <HooksFetchPosts />
      <HooksPosts />
    </main>
  );
}


یک مثال تکمیلی: بازنویسی src\components\counter.jsx با redux hooks

کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کننده‌ی آن‌را که به صورت زیر در فایل src\containers\Counter.js تعریف شده:
import { connect } from "react-redux";

import { decrementValue, incrementValue } from "../actions";
import Counter from "../components/counter";

const mapStateToProps = (state, ownProps) => {
  console.log("CounterContainer->mapStateToProps", { state, ownProps });
  return {
    count: state.counterReducer.count
  };
};

const mapDispatchToProps = {
  incrementValue,
  decrementValue
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);
حذف کرده و با redux hooks جایگزین کنیم. برای این منظور فایل جدید src\components\HooksCounter.jsx را ایجاد می‌کنیم و سپس در ابتدا برای جایگزین کردن قسمت دریافت اطلاعات از this.pros آن:
class Counter extends Component {
  render() {
    console.log("Counter->props", this.props);
    const {
      //counterReducer: { count },
      count,
      incrementValue,
      decrementValue
    } = this.props;
به صورت زیر عمل می‌کنیم:
import React from "react";
import { useDispatch, useSelector } from "react-redux";

import { decrementValue, incrementValue } from "../actions";

const HooksCounter = ({ prop1 }) => {
  const { count } = useSelector(state => {
    console.log("HooksCounter->useSelector", { state, prop1 });
    return {
      count: state.counterReducer.count
    };
  });
  const dispatch = useDispatch();
  return (
  // ...
- متغیر count را با استفاده از useSelector، از شیء state استخراج کرده و با نام خاصیت count بازگشت می‌دهیم.
- اینبار دو action creator مورد استفاده‌ی در متدهای + و - را از ماژول action دریافت کرده‌ایم تا توسط useDispatch مورد استفاده قرار گیرند.
- همچنین دیگر نیازی به ذکر (state, ownProps) نیست. مقدار ownProps، همان props معمولی است که به کامپوننت ارسال می‌شود که برای مثال اینبار نام prop1 را دارد؛ چون هنگامیکه المان کامپوننت HooksCounter را درج و معرفی می‌کنیم، توسط کامپوننت دیگری محصور نشده‌است. تامین آن نیز در فایل src\App.js با درج متداول نام المان کامپوننت HooksCounter و ذکر ویژگی سفارشی prop1 صورت می‌گیرد:
import HooksCounter from "./components/HooksCounter";

//...

function App() {
  const prop1 = 123;
  return (
    <main className="container">
     <HooksCounter prop1={prop1} />
    </main>
  );
}
با این تغییرات، کدهای کامل src\components\HooksCounter.jsx به صورت زیر تکمیل می‌شود که قسمت‌های استفاده از متغیر count و همچنین dispatch دو action creator دریافت شده، در آن مشخص هستند:
import React from "react";
import { useDispatch, useSelector } from "react-redux";

import { decrementValue, incrementValue } from "../actions";

const HooksCounter = ({ prop1 }) => {
  const { count } = useSelector(state => {
    console.log("HooksCounter->useSelector", { state, prop1 });
    return {
      count: state.counterReducer.count
    };
  });
  const dispatch = useDispatch();
  return (
    <section className="card mt-5">
      <div className="card-body text-center">
        <span className="badge m-2 badge-primary">{count}</span>
      </div>
      <div className="card-footer">
        <div className="d-flex justify-content-center align-items-center">
          <button
            className="btn btn-secondary btn-sm"
            onClick={() => dispatch(incrementValue())}
          >
            +
          </button>
          <button
            className="btn btn-secondary btn-sm m-2"
            onClick={() => dispatch(decrementValue())}
          >
            -
          </button>
          <button className="btn btn-danger btn-sm">Reset</button>
        </div>
      </div>
    </section>
  );
};

export default HooksCounter;


مشکل! با استفاده از useSelector، تعداد رندرهای مجدد کامپوننت‌های برنامه افزایش یافته‌است!

برنامه‌ی جاری را پس از این تغییرات  اجرا کنید. با هر بار کلیک بر روی دکمه‌ی fetch posts، حتی کامپوننت شمارشگر درج شده‌ی در صفحه که ربطی به آن ندارد نیز رندر مجدد می‌شود! چرا؟ (این مورد را با مشاهده‌ی کنسول توسعه دهندگان مرورگر می‌توانید مشاهده کنید. در ابتدای متد رندر هر کدام از کامپوننت‌ها، یک console.log قرار داده شده‌است)
زمانیکه اکشنی dispatch می‌شود، useSelector hook با استفاده از مقایسه‌ی ارجاعات اشیاء (strict === reference check)، کار مقایسه‌ی مقدار قبلی و مقدار جدید را انجام می‌دهد. اگر این‌ها متفاوت باشند، کامپوننت را مجبور به رندر مجدد می‌کند. این مورد مهم‌ترین تفاوت بین useSelector hook و متد connect است. متد connect از روش shallow equality checks برای مقایسه‌ی نتایج حاصل از mapStateToProps و تصمیم در مورد رندر مجدد استفاده می‌کند. اما این مقایسه‌ها چه تفاوتی با هم دارند؟
در حالت mapStateToProps، مهم نیست که شیء بازگشت داده شده، دارای یک ارجاع جدید است یا خیر؟ shallow equality checks فقط به معنای مقایسه‌ی خاصیت به خاصیت شیء بازگشت داده شده‌، با نمونه‌ی قبلی است. اما زمانیکه از useSelector hook استفاده می‌کنیم، با بازگشت یک شیء جدید، یعنی یک ارجاع جدید را خواهیم داشت و ... این یعنی اجبار به رندر مجدد کامپوننت‌ها. به همین جهت در این حالت تعداد بار رندر کامپوننت‌ها افزایش یافته‌است، چون خروجی reducerهای تعریف شده‌ی در برنامه، همیشه یک شیء جدید را بازگشت می‌دهند.
برای رفع این مشکل می‌توان از پارامتر دوم متد useSelector که روش مقایسه‌ی اشیاء را مشخص می‌کند، استفاده کرد:
import React from "react";
import { shallowEqual, useSelector } from "react-redux";

import Post from "./Post";

const HooksPosts = () => {
  const { posts, loading, error } = useSelector(
    state => state.postsReducer,
    shallowEqual
  );
  console.log("render HooksPosts");
  return (
  // ...
استفاده از shallowEqual در اینجا سبب خواهد شد تا بجای مقایسه‌ی ارجاعات اشیاء (که همیشه متفاوت خواهند بود؛ چون هربار شیء جدیدی را بازگشت می‌دهیم)، مقادیر تک تک خواص آن‌ها با هم مقایسه شوند.
با اضافه کردن پارامتر shallowEqual به کامپوننت‌های HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمه‌ی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمی‌دهد.

یک نکته: روش دیگر مشاهده‌ی تعداد بار رندر شدن کامپوننت‌ها، استفاده از افزونه‌ی react dev tools و مراجعه به برگه‌ی profiler آن است. روی دکمه‌ی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجه‌ی نهایی نمایش داده شود.

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
مطالب
تهیه قالب برای ارسال ایمیل‌ها در ASP.NET Core توسط Razor Viewها
برای ارسال متن ایمیل‌ها، یا می‌توان یک سری رشته را با هم جمع زد و ارسال کرد و یا یک View را به همراه ViewModel آن، طراحی و سپس این View را تبدیل به یک رشته کرد. روش دوم هم قابلیت طراحی بهتری دارد و هم نگهداری و توسعه‌ی آن ساده‌تر است. در ادامه روش تبدیل Razor Viewهای ASP.NET Core را به یک رشته، بررسی می‌کنیم.


تهیه سرویسی برای رندر کردن Razor Viewها به صورت یک رشته

در ادامه کدهای کامل سرویسی را که توسط RazorViewEngine کار رندر کردن یک View و تبدیل آن‌را به رشته انجام می‌دهد، ملاحظه می‌کنید:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.DependencyInjection;
using System.IO;
using System.Threading.Tasks;
using System;
 
namespace WebToolkit
{
    public static class RazorViewToStringRendererExtensions
    {
        public static IServiceCollection AddRazorViewRenderer(this IServiceCollection services)
        {
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddScoped<IViewRendererService, ViewRendererService>();
            return services;
        }
    }
 
    public interface IViewRendererService
    {
        Task<string> RenderViewToStringAsync(string viewNameOrPath);
        Task<string> RenderViewToStringAsync<TModel>(string viewNameOrPath, TModel model);
    }
 
    /// <summary>
    /// Modified version of: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs
    /// </summary>
    public class ViewRendererService : IViewRendererService
    {
        private readonly IRazorViewEngine _viewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;
        private readonly IHttpContextAccessor _httpContextAccessor;
 
        public ViewRendererService(
                    IRazorViewEngine viewEngine,
                    ITempDataProvider tempDataProvider,
                    IServiceProvider serviceProvider,
                    IHttpContextAccessor httpContextAccessor)
        {
            _viewEngine = viewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
            _httpContextAccessor = httpContextAccessor;
        }
 
        public Task<string> RenderViewToStringAsync(string viewNameOrPath)
        {
            return RenderViewToStringAsync(viewNameOrPath, string.Empty);
        }
 
        public async Task<string> RenderViewToStringAsync<TModel>(string viewNameOrPath, TModel model)
        {
            var actionContext = getActionContext();
 
            var viewEngineResult = _viewEngine.FindView(actionContext, viewNameOrPath, isMainPage: false);
            if (!viewEngineResult.Success)
            {
                viewEngineResult = _viewEngine.GetView("~/", viewNameOrPath, isMainPage: false);
                if (!viewEngineResult.Success)
                {
                    throw new FileNotFoundException($"Couldn't find '{viewNameOrPath}'");
                }
            }
 
            var view = viewEngineResult.View;
            using (var output = new StringWriter())
            {
                var viewDataDictionary = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                {
                    Model = model
                };
 
                var viewContext = new ViewContext(
                    actionContext,
                    view,
                    viewDataDictionary,
                    new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                    output,
                    new HtmlHelperOptions());
                await view.RenderAsync(viewContext).ConfigureAwait(false);
                return output.ToString();
            }
        }
 
        private ActionContext getActionContext()
        {
            var httpContext = _httpContextAccessor?.HttpContext;
            if (httpContext != null)
            {
                return new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor());
            }
 
            httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
            return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        }
    }
}
توضیحات:
اصل این کد متعلق است به مایکروسافت در اینجا. اما در کدهای فوق سه قسمت آن بهبود یافته‌است:
الف) به سازنده‌ی کلاس، سرویس IHttpContextAccessor نیز تزریق شده‌است تا بتوان به HttpContext و اطلاعات آن دسترسی یافت. حالت پیش فرض آن، استفاده از new DefaultHttpContext است. در این حالت اگر در قالب‌های ایمیل‌های خود از Url.Action استفاده کنید، استثنای index out of range مربوط به یافت نشدن مسیریابی‌ها را دریافت خواهید کرد. علت اینجا است که new DefaultHttpContext حاوی اطلاعات مسیریابی درخواست جاری سیستم نیست. به همین جهت توسط IHttpContextAccessor در متد getActionContext، کار مقدار دهی قسمت مسیریابی صورت گرفته‌است.
ب) در کدهای مثال اصلی، فقط viewEngine.FindView ذکر شده‌است. این متد حالت‌های یافتن Viewهایی را به صورت FolderName/ViewName، پشتیبانی می‌کند. اگر بخواهیم یک مسیر کامل را مانند "Areas/Identity/Views/EmailTemplates/_RegisterEmailConfirmation.cshtml/~" ذکر کنیم، کار نمی‌کند. به همین جهت در ادامه، بررسی viewEngine.GetView نیز اضافه شده‌است تا این نقصان را پوشش دهد.
ج) یک overload اضافه‌تر هم جهت رندر یک View بدون مدل نیز به آن اضافه شده‌است.


روش استفاده‌ی از سرویس ViewRenderer

اسمبلی که این سرویس در آن تعریف می‌شود باید دارای وابستگی‌های ذیل باشد:
{ 
    "dependencies": {
        "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.1.0",
        "Microsoft.AspNetCore.Mvc.Razor": "1.1.0"
    }
}
سپس در متد ConfigureServices کلاس آغازین برنامه، سرویس‌های مورد نیاز را اضافه کنید:
public void ConfigureServices(IServiceCollection services)
{
   services.AddRazorViewRenderer();
}
کار متد AddRazorViewRenderer، افزودن سرویس‌های IViewRendererService و همچنین IHttpContextAccessor است.
پس از ثبت سرویس‌های مورد نیاز، اکنون می‌توان سرویس IViewRendererService را به سازنده‌ی کنترلرها و یا کلاس‌های برنامه تزریق و از متدهای RenderViewToStringAsync آن استفاده کرد:
public class RenderController : Controller
{
    private readonly IViewRendererService _viewRenderService;
    public RenderController(IViewRendererService viewRenderService)
    {
        _viewRenderService = viewRenderService;
    }
 
    public async Task<IActionResult> RenderInviteView()
    {
        var viewModel = new InviteViewModel
        {
            UserId = "1",
            UserName = "Vahid"
        };
        var emailBody = await _viewRenderService.RenderViewToStringAsync("EmailTemplates/Invite", viewModel).ConfigureAwait(false);
        //todo: send emailBody
        return Content(emailBody);
    }
}
برای مثال در اینجا در قالب Invite (یا فایل invite.cshtml) واقع در پوشه‌ی EmailTemplates، جهت ساخت متن ایمیل استفاده شده‌است.


چند نکته‌ی تکمیلی در مورد قالب‌های ایمیل

- پیش فرض این سرویس، یافتن Viewها در پوشه‌ی Views است؛ مانند: Views\EmailTemplates\_EmailsLayout.cshtml
مگر اینکه مسیر آن‌را به صورت کامل توسط filename.cshtml/.../~ ذکر کنید و در این حالت ذکر پسوند فایل الزامی است.
- ایمیل‌ها می‌توانند دارای Layout هم باشند. برای مثال فایل Views\EmailTemplates\_EmailsLayout.cshtml را با محتوای ذیل ایجاد کنید:
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Language" content="fa" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style type='text/css'>
     .main {
            font-size: 9pt;
            font-family: Tahoma;
     }
    </style>
</head>
<body bgcolor="whitesmoke" style="font-size: 9pt; font-family: Tahoma; background-color: whitesmoke; direction: rtl;">
   <div class="main">@RenderBody()</div>
</body>
</html>
در اینجا RenderBody@ را مشاهده می‌کنید که محل رندر شدن ایمیل‌های برنامه است.
به علاوه در اینجا جهت ارسال ایمیل‌ها باید هر نوع شیوه نامه‌ای، به صورت صریح قید شود (inline css) و نباید فایل css ایی را لینک کنید.
- پس از اینکه فایل layout خاص ارسال ایمیل‌های خودتان را طراحی کردید، اکنون قالب یکی از ایمیل‌های برنامه، یک چنین فرمتی را پیدا می‌کند که Layout در ابتدای آن ذکر شده‌است:
 @using Sample.ViewModels
@model RegisterEmailConfirmationViewModel
@{
Layout = "~/Views/EmailTemplates/_EmailsLayout.cshtml";
}
با سلام
<br />
 اکانت شما با مشخصات ذیل ایجاد گردید:
....
- حتما تولید لینک‌ها را به صورت مطلق و نه نسبی انجام دهید. اینکار توسط قید صریح protocol صورت می‌گیرد:
 <a style="direction:ltr" href="@Url.Action("Index", "Home", values: new { area = "" }, protocol: this.Context.Request.Scheme)">@Model.EmailSignature</a>
مطالب
لینک‌های هفته سوم دی

وبلاگ‌ها ، سایت‌ها و مقالات ایرانی (داخل و خارج از ایران)

ASP. Net

  • ویدیویی درباره‌ی ASP.Net 4 (بهبودهای حاصل شده در web forms از جهت کار با اسکریپت‌ها خصوصا با تاکید بر jQuery و همچنین ذکر اینکه با آمدن ASP.Net MVC ، وب فرم‌ها کهنه نشده‌ و همچنان توسعه و بهبود داده خواهند شد)، یا مقاله‌ای در این مورد

طراحی و توسعه وب

PHP

اس‌کیوال سرور

سی شارپ

عمومی دات نت

مسایل اجتماعی و انسانی برنامه نویسی

متفرقه

مطالب
سیستم‌های توزیع شده در NET. - بخش چهارم - تعاریف، مزایا و معایب
بدلیل اینکه یکی از مهمترین معایب سیستم‌های توزیع شده، پیچیدگی در طراحی و پیاده سازی این نوع از سیستمها می‌باشد و آشنا بودن ما با تعاریف، خصوصیات، مزایا، معایب، اهداف و اصطلاحات موجود در این نوع سیستمها، باعث کاهش این پیچیدگیها و مدیریت و کنترل بیشتری بر روی این پیچیدگی‌ها می‌شود، پیش نیاز ورود به دنیای سیستم‌های توزیع شده و استفاده از ابزارهای مرتبط با آنها، آشنا بودن با مفاهیم فوق است.

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


تعریف سیستمهای توزیع شده

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


تعاریف سیستم‌های توزیع شده

تعریف اول: سیستمهای توزیع  شده از مجموعه‌ای از سخت افزارها و نرم افزارها که برای رسیدن به یک هدف واحد از طریق شبکه با یکدیگر در ارتباطند تشکیل شده.
تعریف دوم: یک مجموعه از Computerهای مستقل که از نظر کاربر یک Computer و یک سیستم واحد و منسجمند.
تعریف سوم: سیستم‌های توزیع شده در واقع هنری هستند که بوسیله آنها می‌توانید مشکلاتی را که در یک کامپیوتر وجود دارند، با استفاده از چند کامپیوتر رفع کنید و معمولا به این دلیل انتخاب می‌شوند که این مشکلات توسط یک کامپیوتر رفع نمی‌شوند.
تعریف چهارم: زیر سیستمهایی که بصورت همزمان می‌توانند پردازش یک سیستم بزرگ را انجام دهند؛ البته همزمانی که در آن مانند سیستمهای Parallel از یک حافظه مشترک استفاده نشود.
تعریف پنجم: مجموعه ای از پردازشهای مرتبط به هم، بصورتی که هر پردازش از حافظه داخلی مرتبط با خودش استفاده کند و تمام این پردازش‌ها از طریق Message passing در سطح شبکه با یکدیگر در ارتباطند.
تعریف ششم: تقسیم بندی وظایف یک سیستم بزرگ به زیرسیستمهایی که در سخت افزارهای مجزا اجرا می‌شوند و کاربر هیچ تصوری از وجود این زیر سیستمها ندارد.

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


نکات کلیدی یا خصوصیات سیستمهای توزیع شده

1- داشتن یک هدف واحد در سیستم‌های توزیع شده، زیر سیستمهایی که از نظر کاربر یک سیستم واحد و متمرکزند:  این یکی از خصوصیات و نکات کلیدی سیستم‌های توزیع شده‌است که تمام اجزاء و زیرسیستم‌ها در راه رسیدن به یک هدف واحد با یکدیگر در ارتباط اند. ارتباطی که باعث همکاری آنها می‌شود و همکاری که باید بصورت کامل از دید کاربر مخفی بماند (داشتن یک هدف واحد یکی از تفاوتهای سیستم‌های توزیع شده با Cloud distributed systemها می‌باشد. چون در Cloud distributed systemها لزوما تمام اجزا برای رسیدن به یک هدف مشترک با یکدیگر کار نمی‌کنند).

2- کامپیوتر‌های مستقل، حافظه‌های داخلی جداگانه، عدم وجود حافظه مشترک:  یکی از مهمترین تفاوتهای سیستم‌های توزیع شده با سیستمهای Parallel، عدم وجود حافظه مشترک بین پردازش‌های جداگانه است. یعنی در این نوع سیستم‌ها، هر زیرسیستم در یک کامپیوتر مجزا که حافظه داخلی خودش را دارد اجرا می‌شود.

3- تقسیم بندی وظایف: که یکی از نکات کلیدی این نوع سیستمها می‌باشد. تقسیم بندی می‌تواند به هر دلیلی که شما درنظر می‌گیرید صورت بپذیرد. دلایلی برای بالا بردن کارآیی، امنیت، در دسترس بودن، یا حتی دلایل مربوط به Business سیستم شما.

4- ارتباط از طریق شبکه، ارتباطی که از طریق Message passing صورت می‌پذیرد: این خصوصیت در واقع پایه تمام تعاریف سیستم‌های توزیع شده‌است. در سیستم‌های توزیع شده همه چیز از ارتباط‌هایی که از طریق شبکه صورت می‌پذیرد، شروع می‌شود .


مزایای استفاده از سیستم‌های توزیع شده

1- کارآیی بسیار بالاتر: بدلیل همزمان اجرا شدن کارها در سخت افزارهای مختلف، کارآیی این نوع سیستم‌ها بسیار بیشتر از سیستم‌های متمرکز است.
2- قابلیت همکاری بیشتر: بدلیل اینکه این سیستمها ذاتا توزیع شده هستند، با کمترین هزینه و پیچیدگی می‌توانند با سایر سیستمها همکاری لازم را داشته باشند.
3- قابلیت در دسترس بودن و اطمینان بیشتر: در این سیستم‌ها با روشهای مختلفی مانند replication، به راحتی می‌توان این دو قابلیت را در بالاترین سطح قرار داد.
4- مقیاس پذیری: مقیاس پذیری در این سیستمها با قرار دادن کامپوننت‌هایی که قابلیت استفاده مجدد بالایی را دارند در سرور‌های جدید به راحتی و بدون از دسترس خارج شدن سیستم صورت می‌پذیرد.
5- قابلیت گسترش: برای گسترش سیستم و اضافه کردن نیازمندی‌های جدید در این سیستم‌ها، به راحتی می‌توان کامپوننت‌ها و زیرسیستمهای جدیدی را پیاده سازی کرد و بدون از دسترس خارج شدن سیستم و به گونه‌ای که به راحتی با سایر قسمت‌های موجود در ارتباط باشند، آنها را به سیستم اضافه کرد.
6- بهره وری بالاتر و زمان توسعه کمتر: بدلیل تقسیم بندی قسمتهای بزرگ به قسمتهای کوچکتر، تیمهای مختلف می‌توانند بصورت همزمان قسمتهای کوچک را توسعه دهند.
7- قابلیت استفاده مجدد بسیار بالا: در این نوع سیستم‌ها به راحتی می‌توانید از یک زیرسیستم یا کامپوننت خاص که یکبار پیاده سازی شده و در سخت افزار جداگانه‌ای اجرا شده، در تمام Application‌ها استفاده کنید.
8- کاهش هزینه: در مواردی مانند قابلیت استفاده‌ی مجدد بالا و توسعه پذیری سیستم، می‌توانند باعث کاهش هزینه‌ها شوند (در صورت انتخاب نادرست این نوع سیستم، این مزیت می‌تواند تبدیل به یکی از معایب سیستم شود).
9- امنیت: بدلیل اینکه هر زیرسیستم در یک سخت افزار جداگانه اجرا می‌شود که مکان آن از قبل مشخص نیست و همچنین تقسیم بندی قسمت‌هایی که نیاز به امنیت بالایی دارند، می‌تواند بر اساس نیاز و در سخت افزارهایی که حتی به اینترنت هم متصل نیستند، صورت بپذیرد. این نوع سیستم می‌تواند از امنیت بالایی برخوردار باشد (البته در صورت طراحی نادرست، امنیت می‌تواند بعنوان یکی از معایب این نوع سیستمها نیز مطرح شود).


معایب  استفاده از سیستم‌های توزیع شده

1- پیچیدگی در انتخاب، طراحی و پیاده سازی سیستمهای توزیع شده: یکی از اصلی‌ترین معایب سیستم‌های توزیع شده، پیچیدگی‌هایی است که در انتخاب، طراحی و پیاده سازی آنها وجود دارد. به دلیل پیچیدگی‌هایی که در هریک از این قسمت‌ها وجود دارد، در صورت اتخاذ تصمیمات نادرست، در هر یک از این قسمتها اکثر مزایای آنها می‌توانند تبدیل به معایب این نوع سیستم‌ها شوند.

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

3- هزینه طراحی و پیاده سازی بیشتر: دلایل 1 و 2 می‌توانند باعث بالا رفتن هزینه‌های طراحی و پیاده سازی این نوع سیستم‌ها شوند.

4- هزینه‌های بیشتر مرتبط با شبکه: در این نوع سیستم‌ها بدلیل استفاده بیشتر از منابع، مانند سخت افزار‌ها و ابزارهای مرتبط با شبکه، هزینه‌های مرتبط با استفاده از منابع، مانند برق و شبکه بیشتر از سیستم‌های متمرکز است.

5- کاهش امنیت: توزیع نادرست منابع سیستم در سخت افزارهای متفاوت و مدیریت نادرست این منابع باعث کاهش امنیت این نوع سیستم‌ها می‌شود.

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

بدلیل اینکه همیشه قسمتهای مرتبط با پیاده سازی این نوع سیستم‌ها از تعاریف آنها جذابتر است، سعی کرده‌ام در این بخش و بخش بعد که اهداف و اصطلاحات مرتبط با سیستمهای توزیع شده را بررسی میکند، کمتر وارد جزئیات مفاهیم شوم. در صورت نیاز به توضیح بیشتر در مورد قسمت خاصی از این مفاهیم، در قسمت نظرات آن‌ها را مطرح کنید.
مطالب
ارتقاء به 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 پیش از اجرای آن‌ها، اضافه خواهد شد. اطلاعات بیشتر
مطالب
بررسی تغییرات Blazor 8x - قسمت ششم - نکات تکمیلی ویژگی راهبری بهبود یافته‌ی صفحات SSR


در قسمت قبل، در حین بررسی رفتار جزیره‌های تعاملی Blazor Server، نکته‌ی زیر را هم درباره‌ی راهبری صفحات SSR مرور کردیم:
« اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمی‌شود (و یا حتی برنامه‌های MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت می‌گیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم به‌روز رسانی نمی‌کند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.»
در این قسمت، نکات تکمیلی این قابلیت را که به آن enhanced navigation هم گفته می‌شود، بررسی می‌کنیم.


روش غیرفعال کردن راهبری بهبودیافته برای بعضی از لینک‌ها

ویژگی راهبری بهبودیافته فقط در حین هدایت بین صفحات مختلف یک برنامه‌ی Blazor 8x SSR، فعال است. اگر در این بین، کاربری به یک صفحه‌ی غیر بلیزری هدایت شود، راهبری بهبود یافته شکست خورده و سعی می‌کند حالت full document load را پیاده سازی و اجرا کند. مشکل اینجاست که در این حالت دو درخواست ارسال می‌شود: ابتدا حالت راهبری بهبودیافته فعال می‌شود و در ادامه پس از شکست این راهبری، هدایت مستقیم صورت می‌گیرد. برای رفع این مشکل می‌توان ویژگی جدید data-enhance-nav را با مقدار false، به لینک‌های خارجی مدنظر اضافه کرد تا برای این حالت‌ها دیگر ویژگی راهبری بهبودیافته فعال نشود:
<a href="/not-blazor" data-enhance-nav="false">A non-Blazor page</a>


فعالسازی مدیریت بهبودیافته‌ی فرم‌های SSR

در قسمت چهارم این سری با فرم‌های جدید SSR مخصوص Blazor 8x آشنا شدیم. این فرم‌ها هم می‌توانند از امکانات راهبری بهبود یافته استفاده کنند (یعنی مدیریت ارسال آن، توسط fetch API انجام شده و به روز رسانی قسمت‌های تغییریافته‌ی صفحه را Ajax ای انجام دهند)؛ برای نمونه اینبار همانند تصویر زیر، از fetch استاندارد برای ارسال اطلاعات به سمت سرور کمک گرفته می‌شود (یعنی عملیات Ajax ای شده‌؛ بجای یک post-back معمولی):


 اما ... این قابلیت به صورت پیش‌فرض در فرم‌های تعاملی SSR غیرفعال است. چون همانطور که عنوان شد، اگر مقصد این فرم، یک آدرس غیربلیزری باشد، دوبار ارسال فرم صورت خواهد گرفت؛ یکبار با استفاده از fetch API و بار دیگر پس از شکست، به صورت معمولی. اما اگر مطمئن هستید که endpoint این فرم، قطعا یک کامپوننت بلیزری است، بهتر است این قابلیت را در یک چنین فرم‌هایی نیز به صورت زیر فعال کنید:
<form method="post" @onsubmit="() => submitted = true" @formname="name" data-enhance>
    <AntiforgeryToken />
    <InputText @bind-Value="Name" />
    <button>Submit</button>
</form>

@if (submitted)
{
    <p>Hello @Name!</p>
}

@code {

    bool submitted;

    [SupplyParameterFromForm]
    public string Name { get; set; } = "";
}
البته باتوجه به اینکه در اینجا هم می‌توان از EditForm‌ها استفاده کرد، در این حالت فقط کافی است ویژگی Enhance را به آن‌ها اضافه نمائید:
<EditForm method="post" Model="NewCustomer" OnValidSubmit="() => submitted = true" FormName="customer" Enhance>
    <DataAnnotationsValidator />
    <ValidationSummary/>
    <p>
        <label>
            Name: <InputText @bind-Value="NewCustomer.Name" />
        </label>
    </p>
    <button>Submit</button>
</EditForm>

@if (submitted)
{
    <p id="pass">Hello @NewCustomer.Name!</p>
}

@code {
    bool submitted = false;

    [SupplyParameterFromForm]
    public Customer? NewCustomer { get; set; }

    protected override void OnInitialized()
    {
        NewCustomer ??= new();
    }

     public class Customer
    {
        [StringLength(3, ErrorMessage = "Name is too long")]
        public string? Name { get; set; }
    }
}

نکته‌ی مهم: در این حالت فرض بر این است که هیچگونه هدایتی به یک Non-Blazor endpoint صورت نمی‌گیرید؛ وگرنه با یک خطا مواجه خواهید شد.



غیرفعال کردن راهبری بهبودیافته برای قسمتی از صفحه

اگر با استفاده از جاواسکریپت و خارج از کدهای بیلزر، اطلاعات DOM را به‌روز رسانی می‌کنید، ویژگی راهبری بهبودیافته، از آن آگاهی نداشته و به صورت خودکار تمام تغییرات شما را بازنویسی می‌کند. به همین جهت اگر نیاز است قسمتی از صفحه را که مستقیما توسط کدهای جاواسکریپتی تغییر می‌دهید، از به‌روز رسانی‌های این قابلیت مصون نگه‌دارید، می‌توانید ویژگی جدید data-permanent را به آن قسمت اضافه کنید:
<div data-permanent>
    Leave me alone! I've been modified dynamically.
</div>


امکان آگاه شدن از بروز راهبری بهبودیافته در کدهای جاواسکریپتی

اگر به هردلیلی در کدهای جاواسکریپتی خودنیاز به آگاه شدن از وقوع یک هدایت بهبودیافته را دارید (برای مثال جهت بازنویسی تغییرات ایجاد شده‌ی توسط آن)، می‌توانید به نحو زیر، مشترک رخ‌دادهای آن شوید:
<script>
    Blazor.addEventListener('enhancedload', () => {
        console.log('enhanced load event occurred');
    });
</script>


ویژگی جدید Named Element Routing در Blazor 8x

Blazor 8x از ویژگی مسیریابی سمت کلاینت به کمک تعریف URL fragments پشتیبانی می‌کند. به این صورت رسیدن (اسکرول) به یک قسمت از صفحه‌ای طولانی، بسیار ساده می‌شود.
برای مثال المان h2 با id مساوی targetElement را درنظر بگیرید:
<div class="border border-info rounded bg-info" style="height:500px"></div>

<h2 id="targetElement">Target H2 heading</h2>
<p>Content!</p>
در Blazor 8x برای رسیدن به آن، هر سه حالت زیر میسر هستند:
<a href="/counter#targetElement">

<NavLink href="/counter#targetElement">

Navigation.NavigateTo("/counter#targetElement");


معرفی متد جدید Refresh در Blazor 8x

در Blazor 8x، امکان بارگذاری مجدد صفحه با فراخوانی متد جدید NavigationManager.Refresh(bool forceLoad = false) میسر شده‌است. این متد در حالت پیش‌فرض از قابلیت راهبری بهبودیافته برای به روز رسانی صفحه استفاده می‌کند؛ مگر اینکه اینکار میسر نباشد. اگر آن‌را با پارامتر true فراخوانی کنید، full-page reload رخ خواهد داد.
همین اتفاق در مورد متد Navigation.NavigateTo نیز رخ‌داده‌است. این متد نیز در Blazor 8x به صورت پیش‌فرض بر اساس قابلیت راهبری بهبود یافته کار می‌کند؛ مگر اینکه اینکار میسر نباشد و یا پارامتر forceLoad آن‌را به true مقدار دهی کنید.
مطالب
اعمال SEO بر روی AngularJS
در این بخش قصد داریم سئو را بر روی یک برنامه‌ی نوشته شده با آنگلولار و Asp.net Mvc اعمال نماییم. انگولار جی‌اس، صفحات را با  استفاده از جاوااسکریپت رندر میکند، ولی اکثر کرالر‌ها نمیتوانند جاوااسکریپت را اجرا کنند و موقع اجرای صفحات سایت ما  فقط یک div خالی را میبینند.
کاری که سرویس Prerender یا فیلتر سفارشی AjaxCrawlable برای ما انجام میدهد، درخواست‌هایی را که از طرف کرالرها آمده‌است را شناسایی میکند و مانند یک مرورگر، با استفاده از phantomjs آنرا اجرا میکند و نتیجه‌ی کامل صفحات ما را به صورت اچ تی ام ال استاتیک برمی‌گرداند.
فانتوم جی اس، موتور اختصاصی برای شبیه سازی مرورگر مبتنی بر Webkit می‌باشد. فانتوم جی اس را میتوانید بر روی ویندوز، لینوکس و مک نصب نمایید. فانتوم جی اس یک Console در اختیار برنامه نویس قرار می‌دهد که می‌توان توسط آن، برنامه‌های جاوااسکریپت را اجرا نمود. همچنین فانتوم جی اس میتواند اسکرین شاتی را نیز از محتوای وب سایت ما فراهم نماید.
 برای اینکه صفحات انگولار جی اس،ایندکس شوند سه مرحله وجود دارند:
1- به کرالر اطلاع دهیم که رندر کردن سایت، توسط جاوااسکریپت انجام میگردد؛ با اضافه کردن متاتگ زیر در اچ تی ام ال سایت (البته در حالت استفاده HTML5 push state ) :
<meta name="fragment" content="!">
<base href="/">
2- بعد از اضافه کردن متاتگ بالا، کرالر درخواست‌های خود را به صورت زیر به سایت ما ارسال میکند:
http://www.example.com/?_escaped_fragment_=
ما در این مثال از  HTML5 push state  استفاده میکنیم. بنابراین لینکی مانند http://www.example.com/user/123 توسط کرالر به صورت زیر دیده میشود: 
http://www.example.com/user/123?_escaped_fragment_=
3- اچ تی ام ال کاملا رندر شده توسط سایت ما به کرالر ارسال گردد.
برای رندر کردن  اچ تی ام ال صفحات، چندین روش وجود دارد:
روش اول: میتوانیم از سرویس‌های آماده‌ای همچون Prerender.io   استفاده کنیم که سرویسهایی را برای زبانهای مختلف ارائه کرده‌اند. باتوجه به توضیحات نمونه استفاده از آن در Asp.Net Mvc کافیست در سایت Prerender.io  ثبت نام کرده، Token را دریافت کنیم و در کانفیگ برنامه قرار دهیم و در کلاس PreStart قطعه کد زیر را قرار دهیم:
DynamicModuleUtility.RegisterModule(typeof(Prerender.io.PrerenderModule));
مثال استفاده از Prerender.io را میتوانید از این آدرس Simple_Demo_Prerender.zip دانلود نمایید.
 
یکی از ابزارهای مناسب تست کردن اینکه صفحات توسط کرالر ایندکس میشوند یا خیر، برنامه screamingfrog میباشد.
در پنل Ajax آن، صفحات ایندکس شده ما نمایش داده میشوند. لینکی مشابه زیر را در مرورگر اجرا کرده، با ViewPage Source کردن آن میتوانید نتیجه اچ تی ام ال کاملا رندر شده را مشاهده نمایید.
http://www.example.com/user/123?_escaped_fragment_=
نسخه رایگان سرویس Prerender.io تا 250 صفحه را پوشش میدهد.

روش دوم: فیلتر سفارشی AjaxCrawlable. در اولین قدم نیاز به نصب فانتوم جی اس داریم:
<package id="PhantomJS" version="1.9.2" targetFramework="net452" />
<package id="phantomjs.exe" version="1.9.2.1" targetFramework="net452" />
فایل phantomjs.exe را از پوشه packages\PhantomJS.1.9.2\tools\phantomjs\phantomjs.exe یافته و در پوشه bin برنامه قرار دهید. با Attribute زیر هر درخواستی که توسط کرالر ارسال گردد به اکشن returnHTML منتقل میگردد.
برای اینکه خطای معروف A potentially dangerous Request.Form value was detected from the client را دریافت نکنیم، کافیست قسمتهایی از آدرس را که شامل کاراکترهای خاصی مانند :// میباشند، از url حذف کنیم و در اکشن returnHtml قسمتهای حذف شده را  به url  اضافه نماییم.
کرالرها  با مشاهده تگ fragment، تمام لینکها را به همراه کوئری استرینگ _escaped_fragment_  میفرستند، که ما در سرور باید آنرا  با رشته خالی جایگزین نماییم.
 public class AjaxCrawlableAttribute : System.Web.Mvc.ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;
            var url = request.Url.ToString();
            if (request.QueryString[Fragment] != null && !url.Contains("HtmlSnapshot/returnHTML"))
            {
                url = url.Replace("?_escaped_fragment_=", string.Empty).Replace(request.Url.Scheme + "://", string.Empty);
                url = url.Split(':')[1];
                filterContext.Result = new RedirectToRouteResult(
                   new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
Route‌های پیشفرض را با کدهای زیر جایگزین میکنیم:
public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
             name: "HtmlSnapshot",
             url: "HtmlSnapshot/returnHTML/{*url}",
             defaults: new { controller = "HtmlSnapshot", action = "returnHTML", url = UrlParameter.Optional });

            routes.MapRoute(
            name: "SPA",
            url: "{*catchall}",
            defaults: new { controller = "Home", action = "Index" })
        }
 اضافه کردن این فیلتر به فیلترهای Asp.net Mvc 
 public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
ایجاد کنترلر HtmlSnapshot و متد returnHTML :
Url را به عنوان آرگومان به تابع page.open فایل جاوااسکریپتی فانتوم میدهیم و بعد از اجرای کامل، خروجی را درViewData قرار میدهیم 
public ActionResult returnHTML(string url)
        {
            var prefix = HttpContext.Request.Url.Scheme + "://" + HttpContext.Request.Url.Host+":";
            url = prefix+url;
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
            var startInfo = new ProcessStartInfo
            {
                Arguments = string.Format("{0} {1}", Path.Combine(appRoot, "Scripts\\seo.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output1 = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output1.Replace("<!-- ngView:  -->", "").Replace("ng-view=\"\"", "");
            return View();
        }
در فایل renderHtml.cshtml
@{ 
    Layout = null;
}
@Html.Raw(ViewBag.result)
ایجاد فایل seo.js  در پوشه Scripts سایت :
در این بخش webpage  را ایجاد میکنیم و آدرس صفحه را از[system.args[1  دریافت کرده و عملیات کپچر کردن را آغاز میکنیم و بعد از تکمیل اطلاعات در سرور، کد زیر اجرا میشود:
console.log(page.content)

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();;
page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () {

});

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        console.log(page.content);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);
صفحه Layout.Cshtml
<!DOCTYPE html>
<html ng-app="appOne">
<head>
    <meta name="fragment" content="!">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    <base href="/">
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
    <script src="~/Scripts/angular/angular.js"></script>
    <script src="~/Scripts/angular/angular-route.js"></script>
    <script src="~/Scripts/angular/angular-animate.js"></script>
    <script>
        angular.module('appOne', ['ngRoute'], function ($routeProvider, $locationProvider) {
            $routeProvider.when('/one', {
                template: "<div>one</div>", controller: function ($scope) {
                }
            })
            .when('/two', {
                template: "<div>two</div>", controller: function ($scope) {
                }
            }).when('/', {
                template: "<div>home</div>", controller: function ($scope) {
                }
            });
            $locationProvider.html5Mode({
                enabled: true
            });
        });
    </script>
</head>
<body>
    <div id="body">
        <section ng-view></section>
        @RenderBody()
    </div>
    <div id="footer">
        <ul class='xoxo blogroll'>
            <li><a href="one">one</a></li>
            <li><a href="two">two</a></li>
        </ul>
    </div>
</body>
</html>

چند نکته تکمیلی:
* فانتوم جی اس قادر به اجرای لینکهای فارسی (utf-8) نمیباشد.
 * اگر خطای syntax error را دریافت کردید ممکن است پروژه شما در مسیری طولانی در روی هارد دیسک قرار داشته باشد.