مطالب
ASP.NET MVC #10

آشنایی با روش‌های مختلف ارسال اطلاعات یک درخواست به کنترلر

تا اینجا با روش‌های مختلف ارسال اطلاعات از یک کنترلر به View متناظر آن آشنا شدیم. اما حالت عکس آن چطور؟ مثلا در ASP.NET Web forms، دوبار بر روی یک دکمه کلیک می‌کردیم و در روال رویدادگردان کلیک آن، همانند برنامه‌های ویندوزی، دسترسی به اطلاعات اشیاء قرار گرفته بر روی فرم را داشتیم. در ASP.NET MVC که کلا مفهوم Events را حذف کرده و وب را همانگونه که هست ارائه می‌دهد و به علاوه کنترلرهای آن، ارجاع مستقیمی را به هیچکدام از اشیاء بصری در خود ندارند (برای مثال کنترلر و متدی در آن نمی‌دانند که الان بر روی View آن، یک گرید قرار دارد یا یک دکمه یا اصلا هیچی)، چگونه می‌توان اطلاعاتی را از کاربر دریافت کرد؟
در اینجا حداقل سه روش برای دریافت اطلاعات از کاربر وجود دارد:
الف) استفاده از اشیاء Context مانند HttpContext، Request، RouteData و غیره
ب) به کمک پارامترهای اکشن متدها
ج) با استفاده از ویژگی جدیدی به نام Data Model Binding

یک مثال کاربردی
قصد داریم یک صفحه لاگین ساده را طراحی کنیم تا بتوانیم هر سه حالت ذکر شده فوق را در عمل بررسی نمائیم. بحث HTML Helpers استاندارد ASP.NET MVC را هم که در قسمت قبل شروع کردیم، لابلای توضیحات قسمت جاری و قسمت‌های بعدی با مثال‌های کاربردی دنبال خواهند شد.
بنابراین یک پروژه جدید خالی ASP.NET MVC را شروع کرده و مدلی را به نام Account با محتوای زیر به پوشه Models برنامه اضافه کنید:

namespace MvcApplication6.Models
{
public class Account
{
public string Name { get; set; }
public string Password { get; set; }
}
}

یک کنترلر جدید را هم به نام LoginController به پوشه کنترلرهای برنامه اضافه کنید. بر روی متد Index پیش فرض آن کلیک راست نمائید و یک View خالی را اضافه نمائید.
در ادامه به فایل Global.asax.cs مراجعه کرده و نام کنترلر پیش‌فرض را به Login تغییر دهید تا به محض شروع برنامه در VS.NET، صفحه لاگین ظاهر شود.
کدهای کامل کنترلر لاگین را در ادامه ملاحظه می‌کنید:

using System.Web.Mvc;
using MvcApplication6.Models;

namespace MvcApplication6.Controllers
{
public class LoginController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View(); //Shows the login page
}

[HttpPost]
public ActionResult LoginResult()
{
string name = Request.Form["name"];
string password = Request.Form["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
[ActionName("LoginResultWithParams")]
public ActionResult LoginResult(string name, string password)
{
if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
public ActionResult Login(Account account)
{
if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}
}
}

همچنین Viewهای متناظر با این کنترلر هم به شرح زیر هستند:
فایل index.cshtml به نحو زیر تعریف خواهد شد:

@model MvcApplication6.Models.Account
@{
ViewBag.Title = "Index";
}
<h2>
Login</h2>
@using (Html.BeginForm(actionName: "LoginResult", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "LoginResultWithParams", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult(string name, string password)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "Login", controllerName: "Login"))
{
<fieldset>
<legend>Test Login(Account acc)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}

و فایل result.cshtml هم محتوای زیر را دارد:

@{
ViewBag.Title = "Result";
}
<fieldset>
<legend>Login Result</legend>
<p>
@ViewBag.Message</p>
</fieldset>

توضیحاتی در مورد View لاگین برنامه:
در View صفحه لاگین سه فرم را مشاهده می‌کنید. در برنامه‌های ASP.NET Web forms در هر صفحه، تنها یک فرم را می‌توان تعریف کرد؛ اما در ASP.NET MVC این محدودیت برداشته شده است.
تعریف یک فرم هم با متد کمکی Html.BeginForm انجام می‌شود. در اینجا برای مثال می‌شود یک فرم را به کنترلری خاص و متدی مشخص در آن نگاشت نمائیم.
از عبارت using هم برای درج خودکار تگ بسته شدن فرم، در حین dispose شیء MvcForm کمک گرفته شده است.
برای نمونه خروجی HTML اولین فرم تعریف شده به صورت زیر است:

<form action="/Login/LoginResult" method="post">   
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: <input id="Name" name="Name" type="text" value="" /></p>
<p>
Password: <input id="Password" name="Password" type="password" /></p>
<input type="submit" value="Login" />
</fieldset>
</form>

توسط متدهای کمکی Html.TextBoxFor و Html.PasswordFor یک TextBox و یک PasswordBox به صفحه اضافه می‌شوند، اما این For آن‌ها و همچنین lambda expression ایی که بکارگرفته شده برای چیست؟
متدهای کمکی Html.TextBox و Html.Password از نگارش‌های اولیه ASP.NET MVC وجود داشتند. این متدها نام خاصیت‌ها و پارامترهایی را که قرار است به آن‌ها بایند شوند، به صورت رشته می‌پذیرند. اما با توجه به اینکه در اینجا می‌توان یک strongly typed view را تعریف کرد،‌ تیم ASP.NET MVC بهتر دیده است که این رشته‌ها را حذف کرده و از قابلیتی به نام Static reflection استفاده کند (^ و ^).

با این توضیحات، اطلاعات سه فرم تعریف شده در View لاگین برنامه، به سه متد متفاوت قرار گرفته در کنترلری به نام Login ارسال خواهند شد. همچنین با توجه به مشخص بودن نوع model که در ابتدای فایل تعریف شده، خاصیت‌هایی را که قرار است اطلاعات ارسالی به آن‌ها بایند شوند نیز به نحو strongly typed تعریف شده‌اند و تحت نظر کامپایلر خواهند بود.


توضیحاتی در مورد نحوه عملکرد کنترلر لاگین برنامه:

در این کنترلر صرفنظر از محتوای متدهای آن‌ها، دو نکته جدید را می‌توان مشاهده کرد. استفاده از ویژگی‌های HttpPost، HttpGet و ActionName. در اینجا به کمک ویژگی‌های HttpGet و HttpPost در مورد نحوه دسترسی به این متدها، محدودیت قائل شده‌ایم. به این معنا که تنها در حالت Post است که متد LoginResult در دسترس خواهد بود و اگر شخصی نام این متدها را مستقیما در مرورگر وارد کند (یا همان HttpGet پیش فرض که نیازی هم به ذکر صریح آن نیست)، با پیغام «یافت نشد» مواجه می‌گردد.
البته در نگارش‌های اولیه ASP.NET MVC از ویژگی دیگری به نام AcceptVerbs برای مشخص سازی نوع محدودیت فراخوانی یک اکشن متد استفاده می‌شد که هنوز هم معتبر است. برای مثال:

[AcceptVerbs(HttpVerbs.Get)]

یک نکته امنیتی:
همیشه متدهای Delete خود را به HttpPost محدود کنید. به این علت که ممکن است در طی مثلا یک ایمیل، آدرسی به شکل http://localhost/blog/delete/10 برای شما ارسال شود و همچنین سشن کار با قسمت مدیریتی بلاگ شما نیز در همان حال فعال باشد. URL ایی به این شکل، در حالت پیش فرض، محدودیت اجرایی HttpGet را دارد. بنابراین احتمال اجرا شدن آن بالا است. اما زمانیکه متد delete را به HttpPost محدود کردید، دیگر این نوع حملات جواب نخواهند داد و حتما نیاز خواهد بود تا اطلاعاتی به سرور Post شود و نه یک Get ساده (مثلا کلیک بر روی یک لینک معمولی)، کار حذف را انجام دهد.


توسط ActionName می‌توان نام دیگری را صرفنظر از نام متد تعریف شده در کنترلر، به آن متد انتساب داد که توسط فریم ورک در حین پردازش نهایی مورد استفاده قرار خواهد گرفت. برای مثال در اینجا به متد LoginResult دوم، نام LoginResultWithParams را انتساب داده‌ایم که در فرم دوم تعریف شده در View لاگین برنامه مورد استفاده قرار گرفته است.
وجود این ActionName هم در مثال فوق ضروری است. از آنجائیکه دو متد هم نام را معرفی کرده‌ایم و فریم ورک نمی‌داند که کدامیک را باید پردازش کند. در این حالت (بدون وجود ActionName معرفی شده)، برنامه با خطای زیر مواجه می‌گردد:

The current request for action 'LoginResult' on controller type 'LoginController' is ambiguous between the following action methods:
System.Web.Mvc.ActionResult LoginResult() on type MvcApplication6.Controllers.LoginController
System.Web.Mvc.ActionResult LoginResult(System.String, System.String) on type MvcApplication6.Controllers.LoginController

برای اینکه بتوانید نحوه نگاشت فرم‌ها به متدها را بهتر درک کنید، بر روی چهار return View موجود در کنترلر لاگین برنامه، چهار breakpoint را تعریف کنید. سپس برنامه را در حالت دیباگ اجرا نمائید و تک تک فرم‌ها را یکبار با کلیک بر روی دکمه لاگین، به سرور ارسال نمائید.


بررسی سه روش دریافت اطلاعات از کاربر در ASP.NET MVC

الف) استفاده از اشیاء Context

در ویژوال استودیو، در کنترلر لاگین برنامه، بر روی کلمه Controller کلیک راست کرده و گزینه Go to definition را انتخاب کنید. در اینجا بهتر می‌توان به خواصی که در یک کنترلر به آن‌ها دسترسی داریم، نگاهی انداخت:

public HttpContextBase HttpContext { get; }
public HttpRequestBase Request { get; }
public HttpResponseBase Response { get; }
public RouteData RouteData { get; }

در بین این خواص و اشیاء مهیا، Request و RouteData بیشتر مد نظر ما هستند. در مورد RouteData در قسمت ششم این سری، توضیحاتی ارائه شد. اگر مجددا Go to definition مربوط به HttpRequestBase خاصیت Request را بررسی کنیم، موارد ذیل جالب توجه خواهند بود:

public virtual NameValueCollection QueryString { get; } // GET variables
public NameValueCollection Form { get; } // POST variables
public HttpCookieCollection Cookies { get; }
public NameValueCollection Headers { get; }
public string HttpMethod { get; }

توسط خاصیت Form شیء Request می‌توان به مقادیر ارسالی به سرور در یک کنترلر دسترسی یافت که نمونه‌ای از آن‌را در اولین متد LoginResult می‌توانید مشاهده کنید. این روش در ASP.NET Web forms هم کار می‌کند. جهت اطلاع این روش با ASP کلاسیک دهه نود هم سازگار است!
البته این روش آنچنان مرسوم نیست؛ چون NameValueCollection مورد استفاده، ایندکسی عددی یا رشته‌ای را می‌پذیرد که هر دو با پیشرفت‌هایی که در زبان‌های دات نتی صورت گرفته‌اند، دیگر آنچنان مطلوب و روش مرجح به حساب نمی‌آیند. اما ... هنوز هم قابل استفاده است.
به علاوه اگر دقت کرده باشید در اینجا HttpContextBase داریم بجای HttpContext. تمام این کلاس‌های پایه هم به جهت سهولت انجام آزمون‌های واحد در ASP.NET MVC ایجاد شده‌اند. کار کردن مستقیم با HttpContext مشکل بوده و نیاز به شبیه سازی فرآیندهای رخ داده در یک وب سرور را دارد. اما این کلاس‌های پایه جدید، مشکلات یاد شده را به همراه ندارند.


ب) استفاده از پارامترهای اکشن متدها

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

Request.Form
Request.QueryString
Request.Files
RouteData.Values

برای نمونه در متدی که با نام LoginResultWithParams مشخص شده، چون نام‌های دو پارامتر آن، با نام‌های بکارگرفته شده در Html.TextBoxFor و Html.PasswordFor یکی هستند، با مقادیر ارسالی آن‌ها مقدار دهی شده و سپس در متد قابل استفاده خواهند بود. در پشت صحنه هم از همان رکوردهای موجود در Request.Form (یا سایر موارد ذکر شده)، استفاده می‌شود. در اینجا هر رکورد مثلا مجموعه Request.Form، کلیدی مساوی نام ارسالی به سرور را داشته و مقدار آن هم، مقداری است که کاربر وارد کرده است.
اگر همانندی یافت نشد، آن پارامتر با نال مقدار دهی می‌گردد. بنابراین اگر برای مثال یک پارامتر از نوع int را معرفی کرده باشید و چون نوع int، نال نمی‌پذیرد، یک استثناء بروز خواهد کرد. برای حل این مشکل هم می‌توان از Nullable types استفاده نمود (مثلا بجای int id نوشت int? id تا مشکلی جهت انتساب مقدار نال وجود نداشته باشد).
همچنین باید دقت داشت که این بررسی تطابق‌های بین نام عناصر HTML و نام پارامترهای متدها، case insensitive است و به کوچکی و بزرگی حروف حساس نیست. برای مثال، پارامتر معرفی شده در متد LoginResult مساوی string name است، اما نام خاصیت تعریف شده در کلاس Account مساوی Name بود.


ج) استفاده از ویژگی جدیدی به نام Data Model Binding

در ASP.NET MVC چون می‌توان با یک Strongly typed view کار کرد، خود فریم ورک این قابلیت را دارد که اطلاعات ارسالی یکی فرم را به صورت خودکار به یک وهله از یک شیء نگاشت کند. در اینجا model binder وارد عمل می‌شود، مقادیر ارسالی را استخراج کرده (اطلاعات دریافتی از Form یا کوئری استرینگ‌ها یا اطلاعات مسیریابی و غیره) و به خاصیت‌های یک شیء نگاشت می‌کند. بدیهی است در اینجا این خواص باید عمومی باشند و هم نام عناصر HTML ارسالی به سرور. همچنین model binder پیش فرض ASP.NET MVC را نیز می‌توان کاملا تعویض کرد و محدود به استفاده از model binder توکار آن نیستیم.
وجود این Model binder، کار با ORMها را بسیار لذت بخش می‌کند؛ از آنجائیکه خود فریم ورک ASP.NET MVC می‌تواند عناصر شیءایی را که قرار است به بانک اطلاعاتی اضافه شود، یا در آن به روز شود، به صورت خودکار ایجاد کرده یا به روز رسانی نماید.
نحوه کار با model binder را در متد Login کنترلر فوق می‌توانید مشاهده کنید. بر روی return View آن یک breakpoint قرار دهید. فرم سوم را به سرور ارسال کنید و سپس در VS.NET خواص شیء ساخته شده را در حین دیباگ برنامه، بررسی نمائید.
بنابراین تفاوتی نمی‌کند که از چندین پارامتر استفاده کنید یا اینکه کلا یک شیء را به عنوان پارامتر معرفی نمائید. فریم ورک سعی می‌کند اندکی هوش به خرج داده و مقادیر ارسالی به سرور را به پارامترهای تعریفی، حتی به خواص اشیاء این پارامترهای تعریف شده، نگاشت کند.

در ASP.NET MVC سه نوع Model binder وجود دارند:
1) Model binder پیش فرض که توضیحات آن به همراه مثالی ارائه شد.
2) Form collection model binder که در ادامه توضیحات آن‌را مشاهده خواهید نمود.
3) HTTP posted file base model binder که توضیحات آن به قسمت بعدی موکول می‌شود.

یک نکته:
اولین متد LoginResult کنترلر را به نحو زیر نیز می‌توان بازنویسی کرد:
[HttpPost]
[ActionName("LoginResultWithFormCollection")]
public ActionResult LoginResult(FormCollection collection)
{
string name = collection["name"];
string password = collection["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

در اینجا FormCollection به صورت خودکار بر اساس مقادیر ارسالی به سرور توسط فریم ورک تشکیل می‌شود (FormCollection هم یک نوع model binder ساده است) و اساسا یک NameValueCollection می‌باشد.
بدیهی است در این حالت باید نگاشت مقادیر دریافتی، به متغیرهای متناظر با آن‌ها، دستی انجام شود (مانند مثال فوق) یا اینکه می‌توان از متد UpdateModel کلاس Controller هم استفاده کرد:

[HttpPost]
public ActionResult LoginResultUpdateFormCollection(FormCollection collection)
{
var account = new Account();
this.UpdateModel(account, collection.ToValueProvider());

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

متد توکار UpdateModel، به صورت خودکار اطلاعات FormCollection دریافتی را به شیء مورد نظر، نگاشت می‌کند.
همچنین باید عنوان کرد که متد UpdateModel، در پشت صحنه از اطلاعات Model binder پیش فرض و هر نوع Model binder سفارشی که ایجاد کنیم استفاده می‌کند. به این ترتیب زمانیکه از این متد استفاده می‌کنیم، اصلا نیازی به استفاده از FormCollection نیست و متد بدون آرگومان زیر هم به خوبی کار خواهد کرد:

[HttpPost]
public ActionResult LoginResultUpdateModel()
{
var account = new Account();
this.UpdateModel(account);

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

استفاده از model binderها همینجا به پایان نمی‌رسد. نکات تکمیلی آن‌ها در قسمت بعدی بررسی خواهند شد.

مطالب
انقیاد داده‌ها در WPF (بخش دوم)
در ادامه‌ی بخش اول از سری انقیاد داده‌ها در WPF، نحوه‌ی انقیاد داده‌ها در لیست را بررسی می‌کنیم.
• One Way Binding بخش اول
• INPC  بخش اول
• Tow Way Binding بخش اول
• List Binding  بخش دوم
• Element Binding بخش دوم
• Data Conversion بخش دوم

انقیاد در لیست List Binding
در ابتدا متدی با نام GetEmployees را با ساختار زیر، به کلاس Employee ایجاد شده‌ی در بخش اول این سری آموزشی، اضافه می‌کنیم: 
public static ObservableCollection<Employee> GetEmployees()
        {
            var employees = new ObservableCollection<Employee>();
            employees.Add(new Employee() { Name = "Mahdi", Title = "Manager" });
            employees.Add(new Employee() { Name = "Nima", Title = "Teacher" });
            employees.Add(new Employee() { Name = "Rahim", Title = "Assistant" });
            employees.Add(new Employee() { Name = "Saeed", Title = "Administrator" });
            return employees;
        }
در کد بالا از نوع داده‌ی جنریک ObservableCollection برای برگردان لیست کارمندان استفاده کردیم.
توجه ObservableCollection در فضای نام System.Collections.ObjectModel قرار دارد.

ObservableCollection  چیست؟
یک مجموعه‌ی پویا (Dynamic Collection) است که بر اثر عملیات‌هایی همچون به‌روز رسانی، حذف و ایجاد اشیاء در آن، یک اعلان صادر می‌شود و View از این اعلان مطلع می‌شود؛ شبیه به کاری که INotifyPropertyChanged انجام می‌دهد. 
در اینجا کلاس Employee با پیاده سازی اینترفیس INPC (معرفی شده در قسمت قبل) به یک کلاس Observable تبدیل شده است. بدین معنی که هر تغییری در وضعیت خصوصیات خود را اعلان می‌کند. با قرار دادن این شیء Observable در یک مجموعه که خود نیز از نوع Observable می‌باشد، از این پس هر تغییری در مجموعه نیز مستقیما View ما را تحت تاثیر قرار می‌دهد.
برای درک بهتر این موضوع در بخش markup، یک Combobox را قرار می‌دهیم:
<ComboBox Name="President" ItemsSource="{Binding}" FontSize="30"
                  Height="50"
                  Width="550">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}"/>
                        <TextBlock Text="{Binding Title}" Margin="5,0,0,0"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
 </ComboBox>
در کد xaml بالا چند نکته اهمیت دارد:
• در Tag مربوط به تعریف Combobox از طریق خصوصیت ItemSource اعلام کردیم که عملیات DataBinding اتفاق خواهد افتاد. پس Combobox ما به منبع داده‌ی پیش فرض در WPF که همان DataContext است مرتبط می‌شود. برای پر کردن منبع داده نیز همانند بخش قبل از طریق سازنده اقدام می‌کنیم.
 private ObservableCollection<Employee> employees;
        public MainWindow()
        {
            InitializeComponent();
            employees = Employee.GetEmployees();
            DataContext = employees;
        }
• برای نمایش عناصر در ComboBox، کار کمی متفاوت‌تر از عملیات انقیاد در یک TextBlock است. در کنترل‌هایی که قرار است لیستی از داده‌ها را پس از عملیات انقیاد نمایش دهند، می‌بایستی قالب نمایش داده‌ها مشخص شود.
حال باید نحوه‌ی نمایش و اعضایی از مجموعه که قرار است، در کنترل به نمایش در آیند مشخص شوند. در اینجا از Combobox.ItemTemplate برای مشخص کردن قالب و همچنین از DataTemplate، برای مشخص کردن داده‌های منتخب ما از مجموعه، استفاده شده است.
بعد از اجرای برنامه اطلاعات نام و عنوان، در Combobox نمایش داده می‌شوند. برای تست ویژگی Observable بودن مجموعه هم کافی است یک دکمه را بر روی صفحه قرار دهید و یک آیتم جدید را به لیست employees واقع در بخش CodeBehind اضافه کنید. مشاهده خواهید کرد که View ما با تغییر منبع داده، بطور اتوماتیک به‌روز می‌شود.

 انقیاد عناصر Element Binding 
در این بخش قصد داریم از یک کنترل به‌عنوان منبع داده استفاده کنیم. کنترل منتخب ما در این مثال یک Slider می‌باشد. در بخش markup کد زیر را اضافه می‌کنیم:
<Grid>
        <StackPanel Orientation="Horizontal">
            <Slider Name="mySlider" Minimum="0" Maximum="100"
                    Width="300"/>
            <TextBlock Margin="5" Text="{Binding Value,ElementName=mySlider}"/>
        </StackPanel>
    </Grid>
Value خصوصیتی است که مقدار Slider را برمی گرداند. پس از اجرای برنامه و حرکت ولوم اسلایدر مشاهده می‌کنید که مقدار جاری اسلایدر در textblock نمایش داده می‌شود.

Data Conversion 
برای درک بهتر مفهوم تبدیل داده در Data Binding با یک مثال کار را شروع می‌کنیم. به کلاس Employee موجود در بخش قبل، یک خصوصیت جدید را به نام تاریخ تولد، اضافه می‌کنیم:
public DateTime BornDate
        {
            get { return _bornDate; }
            set
            {
                _bornDate = value;
                OnPropertyChanged();
            }
        }
و متدی که لیستی از پرسنل را برای ما تولید می‌کرد، به شکل زیر بازنویسی می‌کنیم:
public static ObservableCollection<Employee> GetEmployees()
        {
            var employees = new ObservableCollection<Employee>();
            employees.Add(new Employee()
            { Name = "Mahdi", Title = "Manager", BornDate = DateTime.Parse("2008/8/8") });
            employees.Add(new Employee()
            { Name = "Nima", Title = "Teacher", BornDate = DateTime.Parse("2012/3/14") });
            employees.Add(new Employee()
            { Name = "Rahim", Title = "Assistant", BornDate = DateTime.Parse("2009/11/18") });
            employees.Add(new Employee()
            { Name = "Saeed", Title = "Administrator", BornDate = DateTime.Parse("2014/7/28") });
            return employees;
        }
حال قصد داریم اطلاعات را در یک ListBox، در بخش Markup نمایش دهیم:
<ListBox ItemsSource="{Binding}"
                 BorderThickness="1" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}" Width="100"/>
                        <TextBlock Text="{Binding Title}" Width="100" Margin="5,0,0,0"/>
                        <TextBlock Text="{Binding BornDate}"  Margin="5,0,0,0"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
 همانطور که قبلا اشاره شد، در نمایش لیست‌ها باید قالب و داده‌های مورد نظر برای نمایش، مشخص شوند.
پس از اجرا، خروجی برنامه به شکل زیر است:
 

 همه چیز همانطور که انتظار داشتیم به نمایش درآمد؛ اما اگر بخواهیم تاریخ میلادی، در زمان نمایش در View، بصورت شمسی نمایش داده شود، چطور؟
عملیات تبدیل داده یا همان Data Conversion در اینجا به کمک ما می‌آید.
به شکل زیر دقت کنید:

در زمانیکه در عملیات Data Binding نوع داده‌ی خصوصیت ما در Source (منبع داده) با نوع داده‌ی خصوصیت ما در target  (کنترل یا View) متفاوت است، به یک مبدل در حین Binding نیاز داریم. این کار را از طریق یک کلاس که اینترفیس IValueConvertor را پیاده سازی کرده است، انجام می‌دهیم.

  Converter به معنای مبدل است و اینجا وظیفه‌ی تبدیل مقدار گرفته شده‌ی از منبع داده را به نوع مورد نظر ما در View، دارد.
در مثال ذکر شده می‌خواهیم تاریخ میلادی موجود در منبع داده را به یک رشته (تاریخ شمسی) تبدیل کنیم و در View نمایش دهیم. (تبدیل نوع داده‌ی میلادی به رشته)
کلاسی را با نام DateConverter ایجاد می‌کنیم و اینترفیس IValueConverter را به شکل زیر پیاده سازی می‌کنیم:
public class DateConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DateTime date = (DateTime)value;
            PersianCalendar pc = new PersianCalendar();
            var persianDate = string.Format
                ($"{pc.GetYear(date)}/{pc.GetMonth(date)}/{pc.GetDayOfMonth(date)}");
            return persianDate;
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
اینترفیس IValueConverter دو متد دارد:
• Convert جهت تبدیل اطلاعات از مبداء به مقصد 
• ConvertBack برای تبدیل اطلاعات از مقصد به مبداء
در متد Convert همانطور که می‌بینید مقدار تاریخ میلادی ارسال شده، به یک رشته تبدیل شده و بازگردانده می‌شود.

حال برای استفاده از این مبدل باید مراحل زیر انجام شود: 
• اضافه کردن فضای نام مبدل به بخش فضاهای نام در View؛ که در اینجا همان آدرس پروژه‌ی جاری است:
 xmlns:local="clr-namespace:DataConversion"
• ایجاد یک شیء از مبدل و اضافه کردن آن به بخش Resource‌ها در view جاری:
<Window.Resources>
        <local:DateConverter x:Key="MyConverter"/>
    </Window.Resources>
markup مربوط به Resource یک پارامتر می‌گیرد و آن هم Key  است. Key کلید آیتم موجود در دیکشنری Resource است. اینجا کلید را MyConverter تعریف کردیم تا در بخش بعدی به‌راحتی بتوانیم از طریق آن اطلاعات را از Resource بازیابی کنیم.
• ارجاع به Static Resource که در مرحله‌ی قبل مقدار دهی کردیم:
<TextBlock Text="{Binding BornDate,Converter={StaticResource MyConverter}}"  Margin="5,0,0,0"/>
پس از طی مراحل بالا، برنامه را مجددا اجرا کنید. اینبار خروجی به شکل زیر است:

مطالب
SQL Server 2008: Script Data

افزونه‌ای برای SQL server 2005 به نام Database Publishing Wizard وجود داشت/دارد که توسعه‌ی آن به ظاهر برای SQL server 2008 متوقف شده است. توسط این افزونه می‌توان رکوردهای یک دیتابیس را به صورت عبارات T-SQL درآورد (هر رکورد را به صورت خودکار تبدیل به یک دستور insert می‌کند). به این صورت کار انتقال دیتا خصوصا به هاست‌هایی که دسترسی مستقیم restore کردن داده را نمی‌دهند، به سادگی صورت می‌گیرد. تنها کافی است خروجی کار یکبار بر روی دیتابیس مقصد اجرا شود تا رکوردهایی دقیقا با همان اطلاعات دیتابیس منبع در آن ایجاد گردند.
این قابلیت اکنون جزئی از management studio اس کیوال سرور 2008 است.
برای استفاده از آن بر روی دیتابیس مورد نظر در management studio 2008 کلیک راست کرده و گزینه زیر را انتخاب کنید:
Tasks -> Generate scripts…

در صفحه ویزاردی که ظاهر می‌شود، بر روی next کلیک کرده و در صفحه‌ی بعدی گزینه script data را یافته و مقدار آن‌را به true تنظیم نمائید (شکل زیر).



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

شایان ذکر است این قابلیت با نگارش‌های پائین‌تر اس کیوال سرور نیز کار می‌کند (برای مثال اتصال به اس کیوال سرور 2000 از طریق management studio 2008).

مطالب
چگونه یک ایمیل مفید خودکار را طراحی کنیم؟

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

1) حتما در انتهای ایمیل خودکار ارسالی، ساعت و تاریخ شمسی ارسال پیام را نیز ذکر کنید.
عموما از آنجائیکه سیستم استاندارد ارسال ایمیل بر اساس تاریخ میلادی است و تقریبا تمام کلاینت‌های دریافت ایمیل موجود نیز توانایی شمسی سازی تاریخ دریافت و ارسال ایمیل را ندارند (مگر با یک سری افزونه و یا دستکاری در سیستم عامل که آنچنان خوشایند و مرسوم نیست)، ذکر تاریخ شمسی در انتهای پیام بسیار مفید خواهد بود و در اکثر اوقات استناد به ایمیل‌های دریافت شده بر اساس تاریخ دریافت آن‌ها است.

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

3) هر چه می‌توانید اطلاعات بیشتری را توسط یک ایمیل خودکار منتقل کنید.
مورد قبل را در نظر بگیرید. ذکر "ارجاع کار جدید ..." در عنوان و سپس مجددا ذکر همین عنوان به عنوان بدنه‌ی ایمیل خودکار به زودی ایمیل‌های شما را تبدیل به نوعی Spam آزار دهنده خواهد کرد. کار جدیدی ارجاع شده است؟ آیا می‌توان خلاصه‌ای از این کار را به همراه ایمیل نیز ارسال کرد تا کاربر حتما برای مشاهده‌ی ریز جزئیات کار به برنامه مراجعه نکند و این ایمیل واقعا ارزش مطالعه را داشته باشد و سبب تسریع در انجام کارها شود؟
برای مثال ذکر کلی این مورد که درخواست مرخصی جدیدی را باید تائید یا رد کنید، کافی نیست. ریز جزئیات مرخصی را هم به همراه ایمیل ارسال کنید.

4) ایمیل شما باید حاوی لینکی جهت باز کردن برنامه‌ی تحت وب مرتبط نیز باشد.
کاری ارجاع شده است؟ بهتر است لینک پویایی را جهت هدایت کاربر به صفحه‌ی مرتبط رسیدگی به همان کار ارجاعی ارسال کنید. به این صورت زحمت او را کمتر کرده و یک مرحله گزارش گیری را حذف خواهید کرد. یا حداقل یک محل مراجعه‌ی کلی بعدی را به این صورت می‌توان ارائه داد.

5) از بکارگیری قسمت from ایی مانند DoNotReply@Site.Com خودداری کنید.
کاربر دریافت کننده‌ی ایمیل باید بداند که در صورت وجود مشکل باید به کجا مراجعه کند؟ چه کسی این ایمیل را ارسال کرده؟
هرچند برنامه به صورت خودکار تمام قسمت‌های این ایمیل ارسالی را تهیه می‌کند اما اگر خبرنامه‌ی تنظیم شده‌ای نیست، حتما شخص ارسال کننده‌ای دارد. یا حداقل یک ایمیل عمومی را برای این مورد تنظیم کنید (ایمیلی که وجود خارجی داشته و هر از چندگاهی بررسی می‌شود).

6) رنگ زمینه و اندازه‌ی قلم مناسبی را انتخاب کنید.
دقیقا برای هر کدام از موارد ذکر شده چندین بار مشکل داشته‌ام! عموما کسانی که ایمیل‌ها را دریافت می‌کنند سن و سال دار هستند. بنابراین انتخاب فونت tahoma با اندازه‌ی 8 یا pt 7 سبب توبیخ زود هنگام شما خواهد شد!
همچنین هر چه ساده‌تر بهتر. دقیقا مشکلات از زمانی آغاز می‌شوند که طرحی را انتخاب کنید یا رنگی را برای زمینه بکار ببرید. اینجا است که هر روز یک سلیقه‌ی تحمیلی را باید پذیرا باشید.

7) دقیقا مشخص کنید که ایمیل دریافتی آیا رونوشت‌ است یا خیر!
همان مبحث ارجاع کار را در نظر بگیرید. پس از اینکه سیستم راه اندازی شد، مدیر یکی از قسمت‌ها چند روز بعد این درخواست را "حتما" ارسال خواهد کرد: رونوشت تمام کارهای ارجاعی به کلیه پرسنل بخش و همچنین ریز اقدامات آن‌ها باید برای بنده نیز ارسال شود.
در اینجا تنها افزودن قسمت CC به ایمیل‌های خودکار کفایت نمی‌کند. حتما به صورت درشت در بالای ایمیل، قبل از شروع بدنه ذکر کنید که ایمیل دریافتی یک رونوشت است. در غیر اینصورت باید پاسخگوی علت دریافت ایمیل‌هایی باشید که به درخواست خودشان CC شده است!

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

9) راهی را برای خلاص شدن از شر دریافت ایمیل‌های خودکار نیز پیش بینی کنید!
همان مورد 7 را در نظر بگیرید. دو روز اول خیلی ذوق خواهند کرد! روز سوم وقتی انبوهی از ایمیل‌ها را دریافت کردند، مشکل شما هم شروع خواهد شد. بنابراین امکان تنظیم دریافت یا عدم دریافت ایمیل را حتما در برنامه قرار دهید. یا حداقل نحوه‌ی ایجاد یک پوشه جدید و فیلتر کردن ایمیل‌های رسیده و هدایت خودکار آن‌ها به این پوشه‌ی جدید را آموزش دهید.


خوب! حالا به نظر شما این ایمیل خودکار ارسالی سایت IDevCenter که اخیرا اضافه شده است چه نمره‌ای را کسب می‌کند؟



- تاریخ شمسی در انتهای ایمیل ندارد.
- عنوان‌ها ثابت هستند.
- هیچ جزئیاتی ارائه نشده است.
- لینک مرتبط دارد.
- قسمت from مناسبی دارد.
- ساده است؛ خوب است! فقط اندازه قلم آن بهتر است یک شماره بزرگتر شود.
- بحث رونوشت اینجا مورد ندارد.
- بحث لاگ ... شخصی است.
- امکان تنظیم دریافت ایمیل پیش بینی شده است.
نمره از 7 : 3.5

مطالب
استفاده از ماژول Remote
همانطور که در مقاله «آغاز کار با الکترون» گفتیم، فرآیند اصلی، تنها فرآیندی است که توانایی استفاده از گرافیک بومی سیستم عامل را دارد. ولی بسیاری از اوقات نیاز است در سمت renderProcess توانایی انجام این کار‌ها را داشته باشیم. در این مقاله قصد داریم که همان دیالوگ‌های open و save را از طریق Render Process اجرا نماییم.
الکترون برای اینکار از یک ماژول به نام remote استفاده می‌کند که وظیفه آن برقراری ارتباط IPC از Render Process به Main Process است و مواردی را که لازم است، در اختیار شما قرار می‌دهد. در این شیوه لازم نیست شما مرتبا به ارسال پیام بپردازید، بلکه این ارتباطات را ماژول remote فراهم می‌کند. این مورد شبیه به سیستم RMI در جاواست.

برای استفاده از remote در فایل html، کدهای زیر را در تگ اسکریپت اضافه می‌کنیم:
  const remote=require("electron").remote;
    const dialog=remote.dialog;
اینبار هم مانند قسمت قبلی، کدها را به شیوه دیگری انتساب دادیم. قصد ما از تغییر این رویه این است که با انواع حالت‌های انتساب اشیاء، آشنا شویم. بعد از آن توابع زیر را اضافه می‌کنیم:
  function OpenDialog()
    {
      dialog.showOpenDialog({
        title:'باز کردن فایل متنی',
         properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
        ,filters:[
        {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
        {name:'جهت تست' , extensions:['doc','docx']}
         ]
      },
        (filename)=>{
          if(filename===undefined)
             return;
             var content=  fs.readFileSync(String(filename),'utf8');
             document.getElementById("TextFile").value=content;
    });
    }

    function SaveDialog()
    {
      dialog.showSaveDialog({
        title:'باز کردن فایل متنی',
         properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
        ,filters:[
        {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
         ]
      },
        (filename)=>{
          if(filename===undefined)
             return;
             var content=document.getElementById("TextFile").value;
             fs.writeFileSync(String(filename),content,'utf8');
       });
    }
برای استفاده از این توابع، کدهای زیر را نیز به فایل اضافه می‌کنیم تا دکمه‌های open و save به صفحه اضافه شوند:
<button onclick="OpenDialog();" > Open File</button>
<button onclick="SaveDialog();" > Save File</button>
حالا برنامه را اجرا و تست کنید.

عبارت remote شامل متدهای فراوانی است که تعدادی از آن‌ها را بر می‌شماریم:
remote.getCurrentWindow()
شیء BrowserWindow صفحه جاری را باز می‌گرداند.

remote.getCurrentWebContents()
شیء webContents صفحه جاری را باز می‌گرداند.

remote.getGlobal(name)
این متد، دسترسی به شیء global را داراست و یکی از اشیاء ارتباطی بین Main Process و RenderProcess است که می‌تواند هر نوع داده‌ای را جابجا نماید. برای مشاهده بهتر از نحوه کارکرد این متد کد زیر را مشاهده نمایید:
Main Process
global.testData={year:1395};

Render Process
alert(remote.getGlobal("testData").year);
از این پس هر موقع renderProcess به این کد برسد، پیام 1395 را روی صفحه نمایش خواهد داد.

remote.process
شیء، process را از main process دریافت می‌کند و با کد زیر برابر است. ولی مزیت این متد این است که از کش نیز استفاده می‌نماید.
remote.getGlobal('process')

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

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


نمایی از زنجیره تامین فرآورده‌های دارویی و نحوه فراخوانی سرویس آمارنامه



در این سیستم چالش‌های بسیار مهمی وجود دارند که پس از بررسی‌های انجام شده، برای هر یک راه حلی ارائه خواهد شد:

چالش اول: در دسترس بودن سیستم

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

ارائه راه حل:

بدلیل اینکه احتمال از دسترس خارج شدن یک سرور همیشه وجود دارد، این چالش به تنهایی می‌تواند دلیل محکمی برای پیاده سازی سیستم بصورت توزیع شده باشد. برای حل این مشکل می‌توانیم از روش Active/Standby استفاده کنیم. به این صورت که چند کپی از سرویس روی چند سرور داشته باشیم که هر لحظه یکی از این سرور‌ها فعال باشد. با از دسترس خارج شدن سرور Active، یکی از سرور‌های Standby فعال شود و درخواست‌های جدید برای این سرور ارسال شوند.


این روش تنها قابلیت در دسترس بودن سیستم را افزایش می‌دهد و هیچ تاثیری روی کارآیی سیستم ندارد.

 برای رفع مشکل فوق، از روش Replicate روی یک یا چند Cluster استفاده می‌کنیم. یعنی چند کپی از سرویس، روی چند سرور داشته باشیم؛ به این صورت که همه آنها فعال باشند. درخواست‌ها با الگوریتمی که انتخاب می‌کنیم، از طریق Load Balancer بین این Node‌ها پخش می‌شوند. با این روش، هم کارآیی سیستم بالا می‌رود و هم همیشه Nodeهایی وجود دارند که جای Node‌های از دسترس خارج شده را بگیرند.


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



(در هر صورت بهترین راه حل برای این چالش، استفاده از سیستم‌های توزیع شده است.)


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

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

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

ارائه راه حل:

بدلیل تعداد بسیار زیاد درخواست‌ها و بالا رفتن این تعداد، بصورت لحظه‌ای و حیاتی بودن دسترسی به این سیستم، سیستم باید قابلیت این را داشته باشد که بدون از دسترس خارج شدن، اولا درخواست‌های جاری را پاسخ دهد، دوما همیشه آمادگی لازم را برای افزایش تعداد درخواست‌ها، داشته باشد. یعنی به هیچ وجه Scale-up به‌تنهایی پاسخگوی نیاز ما نیست و برای رفع این مشکل باید از Scale-out کمک بگیریم. یعنی با افزایش تعداد درخواست‌ها، بدون از دسترس خارج شدن سیستم و با کمترین هزینه و پیچیدگی، Node‌هایی به سیستم اضافه کنیم که قسمتی از بار پردازشی در آنها انجام شود.


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


چالش سوم: حجم زیاد هر درخواست و زمان زیاد مورد نیاز برای پردازش آن 

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

ارائه راه حل: 

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


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


چالش چهارم: حجم بسیار زیاد و رو به افزایش داده‌های سیستم

داده‌های این سیستم ذاتا همیشه و در هر شرایطی رو به افزایش هستند و هیچگاه جریان داده، در این سیستم قطع نمی‌شود. با توجه به تعداد کاربران، تعداد درخواست و نوع داده، ما با حجم داده‌ی بسیار زیادی روبرو هستیم که پایانی ندارند.

ارائه راه حل:

با توجه به حیاتی بودن دسترسی به سیستم و سایر چالش‌هایی که در قسمت‌های قبلی ذکر شد، در صورتیکه حتی تمام قسمتهای قبل را به‌درستی طراحی و پیاده سازی کنیم، اگر برای این چالش راه حل درستی را ارائه ندهیم، تمامی راه حل‌های قبلی که ارائه کردیم، بی فایده می‌باشند. چون با از دسترس خارج شدن Database، کل سیستم از دسترس خارج می‌شود.

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

نتیجه گیری این است که تقریبا تمام چالش‌هایی که برای سرویس وجود داشت، برای Database نیز وجود دارد. به همین دلیل باید Database نیز بصورت توزیع شده پیاده سازی شود:



این طراحی تقریبا تمامی قابلیتهای طراحی سرویسمان را دارد. یعنی با افزایش تعداد درخواست، یا کم شدن فضای ذخیره سازی در هر یک از Nodeها، ما این قابلیت را داریم که Nodeهایی را به آن اضافه کنیم. همچنین بدلیل اینکه داده‌های ما در دو یا چند Node کپی شده‌اند، با از دسترس خارج شدن هر Node همیشه Nodeهایی وجود دارند که جای Node معیوب را بگیرند؛ تا زمانیکه Node معیوب دوباره به سیستم بازگردد.

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


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

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

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

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

4- تعداد درخواست‌های از سیستم: تعداد درخواست‌ها در اکثر موارد وابستگی بسیار زیادی به تعداد یا نوع کاربران دارد. پوشش دادن تعداد زیاد درخواست، بصورت متناوب و رو به افزایش می‌تواند فاکتور بسیار مهمی در انتخاب یک سیستم توزیع شده باشد.

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

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

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

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

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

علاوه بر موارد فوق مواردی را مانند Internet of things یا همان IOT  که پایه و اساس آن سیستم‌های توزیع شده‌است، یا مواردی را مانند Machine learning که می‌تواند بصورت توزیع شده پیاده سازی شود، نیز در نظر بگیرید.

با در نظر گرفتن تمام موارد فوق و شرایط اختصاصی سیستمی که طراحی می‌کنید، سعی کنید بهترین انتخاب را انجام دهید.
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت دوم - جمع آوری اطلاعات آماری کوئری‌ها توسط Extended Events
همانطور که در قسمت قبل نیز بررسی کردیم، Management Studio برای جمع آوری اطلاعات آماری کوئری‌های زنده بسیار مفید است؛ اما تهیه‌ی آن دستی است. باید کوئری را اجرا کرد و سپس مراحلی را طی نمود تا به نتایج آماری حاصل از کوئری‌ها رسید و همچنین دست آخر باید از نتایج آن نیز یک خروجی دستی را تهیه کرد. روش دیگری نیز برای جمع آوری اطلاعات آماری کوئری‌ها در SQL Server توسط Extended Events/Trace وجود دارد که به ازای هر کوئری، قابل استخراج است. علاوه بر آن می‌توان از Dynamic management objects و یا Query store نیز استفاده کرد. این دو برخلاف Extended Events/Trace، اطلاعات تجمعی گروهی از کوئری‌ها را بازگشت می‌دهند. همچنین در اینجا performance monitor نیز می‌تواند مورد استفاده قرار گیرد؛ اما محدوده‌ی دید آن کل بانک اطلاعاتی است.


Extended Events/Trace

Extended Events، زیر ساخت مدیریت رخ‌دادها در SQL Server است. برای مثال در نگارش 2016 آن بیش‌از 300 رخ‌داد در SQL Server تعریف شده‌اند و زمانیکه در مورد اجرای کوئری‌ها بحث می‌کنیم، این رخ‌دادها بیشتر مدنظر ما هستند:
sql_statement_completed
sp_statement_completed
rpc_completed
sql_batch_completed
کار آن‌ها دریافت اطلاعاتی در مورد logical reads، میزان مصرف CPU، مدت زمان اجرای کوئری‌ها و امثال آن‌ها است. در این بین، دو مورد اول بیش از همه مورد استفاده قرار می‌گیرند.
علاوه بر این‌ها، رخ‌دادهای بسط یافته‌ی زیر را نیز می‌توان مورد استفاده قرار داد:
query_post_compilation_showplan
query_post_execution_showplan
query_pre_execution_showplan
اما به علت هزینه‌بر بودن تولید execution plan به ازای هر کوئری، آنچنان مورد استفاده قرار نمی‌گیرند.


استفاده از Extended Events برای جمع آوری اطلاعات آماری کوئری‌ها

برای آزمایش نحوه‌ی کار با Extended Events، ابتدا رویه‌ی ذخیره شده‌ی زیر را ایجاد می‌کنیم:
USE [WideWorldImporters];
GO

DROP PROCEDURE IF EXISTS [Application].[usp_GetCountryInfo];
GO

CREATE PROCEDURE [Application].[usp_GetCountryInfo]
    @Country_Name NVARCHAR(60)
AS

SELECT *
FROM [Application].[Countries] [c]
    JOIN [Application].[StateProvinces] [s]
    ON [s].[CountryID] = [c].[CountryID]
WHERE [c].[CountryName] = @Country_Name;
GO
این کوئری شبیه به کوئری‌است که در قسمت قبل مورد استفاده قرار گرفت؛ با این تفاوت که به همراه یک * SELECT است که استفاده‌ی از آن توصیه نمی‌شود و در اینجا بیشتر جهت بررسی کارآیی این کوئری، تعریف شده‌است.
سپس یک سشن Extended Events سفارشی را به صورت زیر ایجاد می‌کنیم:
/*
Create XE session to capture sql_statement_completed
and sp_statement_completed
*/
IF EXISTS (
SELECT *
FROM sys.server_event_sessions
WHERE [name] = 'QueryPerf')
BEGIN
    DROP EVENT SESSION [QueryPerf] ON SERVER;
END
GO

CREATE EVENT SESSION [QueryPerf] 
ON SERVER 
ADD EVENT sqlserver.sp_statement_completed(WHERE ([duration]>(1000))),
ADD EVENT sqlserver.sql_statement_completed(WHERE ([duration]>(1000)))
ADD TARGET package0.event_file(SET filename=N'C:\Temp\QueryPerf\test.xel',max_file_size=(256))
WITH (
  MAX_MEMORY=16384 KB,EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS,
  MAX_DISPATCH_LATENCY=5 SECONDS,MAX_EVENT_SIZE=0 KB,
  MEMORY_PARTITION_MODE=NONE,TRACK_CAUSALITY=OFF,STARTUP_STATE=OFF);
GO
در این سشن، رخ‌دادهای sp_statement_completed و sql_statement_completed مورد استفاده قرار گرفته‌اند. هر کدام نیز بر اساس مدت زمان اجرای کوئری، فیلتر شده‌اند. در اینجا عدد 1000، یعنی یک میلی ثانیه که عدد بسیار کوچکی است؛ اما برای دمو، مفید است. نتیجه‌ی عملیات نیز در مسیر C:\Temp\QueryPerf ذخیره خواهد شد.

سپس نیاز است تا این سشن را که QueryPerf نام دارد، در قسمت management->extended events، اجرا و آغاز کرد:


در ادامه ابتدا بر روی بانک اطلاعاتی WideWorldImporters، کلیک راست کرده و یک پنجره‌ی new query جدید را ایجاد می‌کنیم:
WHILE 1 = 1
BEGIN
   EXECUTE [Application].[usp_GetCountryInfo] N'United States';
END
در این پنجره با یک حلقه‌ی بی‌پایان، رویه‌ی ذخیره شده‌ای را که ایجاد کردیم، بارها و بارها اجرا خواهیم کرد (نکته‌ی «عدم نمایش ردیف‌های بازگشت داده شده‌ی توسط کوئری در حین جمع آوری اطلاعات آماری» قسمت قبل را هم مدنظر داشته باشید).

سپس مجددا یک پنجره‌ی new query دیگر را باز می‌کنیم:
WHILE 1 = 1
BEGIN
    SELECT
        [s].[StateProvinceName],
        [s].[SalesTerritory],
        [s].[LatestRecordedPopulation],
        [s].[StateProvinceCode]
    FROM [Application].[Countries] [c]
        JOIN [Application].[StateProvinces] [s]
        ON [s].[CountryID] = [c].[CountryID]
    WHERE [c].[CountryName] = 'United States';
END
این کوئری شبیه به رویه‌ی ذخیره شده‌ای است که ایجاد کردیم؛ اما یک کوئری Ad Hoc و غیر پارامتری می‌باشد.

کوئری‌های هر دو پنجره را به صورت مجزایی اجرا کنید. سپس در قسمت management->extended events، بر روی سشن QueryPerf کلیک راست کرده و گزینه‌ی View live data را انتخاب کنید:


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

یک نکته: در اینجا در قسمت Details، اگر بر روی هر ردیف کلیک کنید، امکان انتخاب و نمایش آن در لیست بالای صفحه توسط گزینه‌ی Show Column in table وجود دارد.

در آخر در قسمت management->extended events، بر روی سشن QueryPerf کلیک راست کرده و گزینه‌ی Stop Session را انتخاب کنید. اکنون اگر به پوشه‌ی C:\Temp\QueryPerf مراجعه کنید، فایل xel حاوی اطلاعات این گزارش را نیز می‌توانید مشاهده نمائید (به ازای هربار اجرای این سشن، یک فایل جدید را تولید می‌کند).


 این فایل توسط Management Studio قابل گشودن و بررسی است و دقیقا همان نمای گزارش live data را به همراه دارد.
نظرات مطالب
Blazor 5x - قسمت نهم - مبانی Blazor - بخش 6 - ساده سازی تعاریف ویژگی‌های المان‌ها و انتقال پارامترها به چندین زیر سطح
یک نکته‌ی تکمیلی: روش دیگری برای انتقال اطلاعات بین کامپوننت‌هایی که در یک سلسله مراتب قرار ندارند
نظرات مطالب
تغییر عملکرد و یا ردیابی توابع ویندوز با استفاده از Hookهای دات نتی
در قسمت API Filter آن، تمام موارد موجود را تیک بزنید. به این صورت تمام توابع WIN32 API تعریف شده در اینجا تحت نظر قرار می‌گیرند. زمانیکه فراخوانی شدند در قسمت لاگ صفحه، ریز جزئیات آن‌ها (تمام ورودی‌ها و خروجی‌های توابع) قابل بررسی و ذخیره خواهد بود.
نظرات مطالب
EF Code First #6
سلام.ممنون به خاطر مطالبی که قرار میدید.
 من در قسمت دوم به خطایی برخورد کردم که تو این قسمت هم دوباره با این خطا مواجه شدم.

Inconsistent accessibility: property type 'System.Data.Entity.DbSet' is less accessible than property 'EF_Sample03.DataLayer.Sample03Context.Projects'

اشکال کار چی می تونه باشه؟