مطالب
ASP.NET MVC #9

مروری بر HTML Helpers استاندارد مهیا در ASP.NET MVC

یکی از اهداف وجودی Server controls در ASP.NET Web forms، رندر خودکار HTML است. برای مثال Menu control، TreeView control، GridView و امثال آن کار تولید تگ‌های table، tr و بسیاری موارد دیگر را در پشت صحنه برای ما انجام می‌دهند. اما در ASP.NET MVC، هدف رسیدن به یک markup ساده و تمیز است که 100 درصد بر روی اجزای آن کنترل داشته باشیم و این مورد به صورت ضمنی به این معنا است که در اینجا تمام این HTMLها را باید خودمان تولید کنیم. البته در عمل خیر. یک نمونه از آن‌را در قسمت قبل مشاهده کردیم که چطور می‌توان منطق تولید تگ‌های HTML را کپسوله سازی کرد و بارها مورد استفاده قرار داد. به علاوه فریم ورک ASP.NET MVC نیز به همراه تعدادی HTML helper توکار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غیره که کار تولید تگ‌های HTML ضروری و پایه را برای ما ساده می‌کنند.
یک مثال:
@Html.ActionLink("About us", "Index", "About")

در اینجا از متدی به نام ActionLink استفاده شده است. شیء Html هم وهله‌ای از کلاس HtmlHelper است که در تمام Viewها قابل دسترسی می‌باشد.
در این متد،‌ اولین پارامتر، متن نمایش داده شده به کاربر را مشخص می‌کند، پارامتر سوم، نام کنترلری است که مورد استفاده قرار می‌گیرد و پارامتر دوم، نام متد یا اکشنی در آن است که فراخوانی خواهد شد (البته هر کدام از این HtmlHelperها به همراه تعداد قابل توجهی overload هم هستند).
زمانیکه این صفحه را رندر کنیم، به خروجی زیر خواهیم رسید:
<a href="/About">About us</a>

در این لینک نهایی خبری از متد Index ایی که معرفی کردیم، نیست. چرا؟
متد ActionLink بر اساس تعاریف پیش فرض مسیریابی برنامه، سعی می‌کند بهترین خروجی را ارائه دهد. مطابق تعاریف پیش فرض برنامه، متد Index، اکشن پیش فرض کنترلرهای برنامه است. بنابراین ضرورتی به ذکر آن ندیده است.

مثالی دیگر:
همان کلاس‌های Product و Products قسمت هفتم را در نظر بگیرید (قسمت بررسی «ساختار پروژه مثال جاری» در آن مثال). همچنین به اطلاعات «نوشتن HTML Helpers ویژه، به کمک امکانات Razor» قسمت هشتم هم نیاز داریم.
اینبار می‌خواهیم بجای نمایش لیست ساده‌ای از محصولات،‌ ابتدا نام آن‌ها را به صورت لینک‌هایی در صفحه نمایش دهیم. در ادامه پس از کلیک کاربر روی یک نام، توضیحات بیشتری از محصول انتخابی را در صفحه‌ای دیگر ارائه نمائیم. کدهای View ما اینبار به شکل زیر تغییر می‌کنند:

@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>

@GetProductsList(@Model)

توضیحات:
ابتدا یک helper method را تعریف کرده‌ایم و به کمک Html.ActionLink، از نام و شماره محصول، جهت تولید لینک‌های نمایش جزئیات هر یک از محصولات کمک گرفته‌ایم. بنابراین در کنترلر خود نیاز به متد جدیدی به نام Details خواهیم داشت که پارامتری از نوع ProductNumber را دریافت می‌کند. سپس جزئیات این محصول را یافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومی که در متد ActionLink بکارگرفته شده در اینجا مشاهده می‌کنید، یک anonymously typed object است و توسط آن خواصی را تعریف خواهیم کرد که توسط تعاریف مسیریابی تعریف شده در فایل Global.asax.cs،‌ قابل تفسیر و تبدیل به لینک‌های مرتبط و صحیحی باشد.
اکنون اگر این مثال را اجرا کنیم، اولین لینک تولیدی آن به این شکل خواهد بود:
http://localhost/Home/Details/D123

در اینجا به یک نکته مهم هم باید دقت داشت؛ نام کنترلر به صورت خودکار به این لینک اضافه شده است. بنابراین بهتر است از ایجاد دستی این نوع لینک‌ها خودداری کرده و کار را به متدهای استاندارد فریم ورک واگذار نمود تا بهترین خروجی را دریافت کنیم.
البته اگر الان بر روی این لینک کلیک نمائیم، با پیغام 404 مواجه خواهیم شد. برای تکمیل این مثال، متد Details را به کنترلر تعریف شده اضافه خواهیم کرد:

using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;

namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}

public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}

در متد Details، ابتدا ProductNumber دریافت شده و سپس شیء محصول متناظر با آن، به View این متد، بازگشت داده می‌شود. اگر بر اساس ورودی دریافتی، محصولی یافت نشد، کاربر را به View ایی به نام Error که در پوشه Views/Shared قرار گرفته است، هدایت می‌کنیم.
برای اضافه کردن این View هم بر روی متد کلیک راست کرده و گزینه Add view را انتخاب کنید. چون یک شیء strongly typed از نوع Product را قرار است به View ارسال کنیم (مانند مثال قسمت پنجم)، می‌توان در صفحه باز شده تیک Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب کرد و در قسمت Scaffold template هم Details را انتخاب نمود. به این ترتیب Code generator توکار VS.NET قسمتی از کار تولید View را برای ما انجام داده و بدیهی است اکنون سفارشی سازی این View تولیدی که قسمت عمده‌ای از آن تولید شده است، کار ساده‌ای می‌باشد:

@model MvcApplication5.Models.Product

@{
ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
<legend>Product</legend>

<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>

<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>

<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>

در اینجا کدهای مرتبط با View نمایش جزئیات محصول را مشاهده می‌کنید که توسط VS.NET به صورت خودکار از روی مدل انتخابی تولید شده است.
اکنون یکبار دیگر برنامه را اجرا کرده و بر روی لینک نمایش جزئیات محصولات کلیک نمائید تا بتوان این اطلاعات را در صفحه‌ی بعدی مشاهده نمود.


یک نکته:
اگر سعی کنیم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهیم، به متد Html.ActionLink دسترسی نخواهیم داشت. چرا؟
پیغام خطایی که ارائه می‌شود این است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink' 

به این معنا که در وهله‌ای از شیء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink می‌گردد. در حالیکه ActionLink مورد نظر به کلاس System.Web.Mvc.HtmlHelper مرتبط می‌شود.
یک راه حل آن به صورت زیر است. به هر متد helper یک آرگومان WebViewPage page را اضافه می‌کنیم (به همراه دو فضای نامی که به ابتدای فایل اضافه می‌شوند)

@using System.Web.Mvc
@using System.Web.Mvc.Html

@using MvcApplication5.Models

@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
سپس برای استفاده از آن در یک View خواهیم داشت:
@MyHelpers.GetProductsList(this, @Model)


متد ActionLink و عبارات فارسی

متد ActionLink آدرس‌های وبی را که تولید می‌کند، URL encoded هستند. برای نمونه اگر رشته‌ای که قرار است به عنوان پارامتر به اکشن متد ما ارسال شود، مساوی Hello World است، آن‌را به صورت Hello%20World در صفحه درج می‌کند. البته این مورد مشکلی را در سمت متدهای کنترلرها ایجاد نمی‌کند، چون کار URL decoding خودکار است. اما ... اگر مقداری که قرار است ارسال شود مثلا «مقدار یک» باشد، آدرس تولیدی این شکل را خواهد داشت:

http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83

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

<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>

IE حین نمایش نهایی آن، آن‌را فارسی نشان خواهد داد. حتی زمانیکه کاربر بر روی آن کلیک کند، به صورت خودکار کاراکترهایی را که لازم است encode نماید، به نحو صحیحی در URL نهایی قابل مشاهده در نوار آدرس‌ها ظاهر خواهد کرد. برای مثال %20 را به صورت خودکار اضافه می‌کند و نگرانی از این لحاظ وجود نخواهد داشت که الان بین دو کلمه فاصله‌ای وجود دارد یا خیر (مرورگرهای دیگر هم دقیقا همین رفتار را در مورد لینک‌های داخل صفحه دارند).
خلاصه این توضیحات متد کمکی زیر است:

@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}

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

@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}

ضمن اینکه باید درنظر داشت کلا این نوع طراحی مشکل دارد! برای مثال فرض کنید که در این مثال، جزئیات، نمایش دهنده مطلب ارسالی در یک بلاگ است. یعنی یک سری عنوان و جزئیات متناظر با آن‌ها در دیتابیس وجود دارند. اگر آدرس مطالب به این شکل باشد http://site/blog/details/text، به این معنا است که این text مساوی است با primary key جدول بانک اطلاعاتی. یعنی وبلاگ نویس سایت شما فقط یکبار در طول عمر این برنامه می‌تواند بگوید «سال نو مبارک!». دفعه‌ی بعد به علت تکراری بودن، مجاز به ارسال پیام تبریک دیگری نخواهد بود! به همین جهت بهتر است طراحی را به این شکل تغییر دهید http://site/blog/details/id/text. در اینجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشایند بانک اطلاعاتی و Text هم برای خوشایند موتورهای جستجو در این URL قرار دارند. مطابق تعاریف مسیریابی برنامه، Text فقط حالت تزئینی داشته و پردازش نخواهد شد.
از این نوع ترفندها زیاد به کار برده می‌شوند. برای نمونه به URL مطالب انجمن‌های معروف اینترنتی دقت کنید. عموما یک عدد را به همراه text مشاهده می‌کنید. عدد در برنامه پردازش می‌شود، متن هم برای موتورهای جستجو درنظر گرفته شده است.



مطالب
خلاصه‌ای کاربردی در مورد Observable collection

Observable collection در WPF را می‌توان نوعی لیست جنریک ویژه تعریف کرد که زمانیکه به کنترلی بایند شد، کنترل را از تغییرات خودش آگاه می‌کند. برای مثال اگر آیتمی به این لیست اضافه شد بلافاصله آن آیتم را در کنترل مقید به آن نیز خواهید دید، به همین ترتیب در مورد ویرایش و یا حذف یک آیتم، بدون نیاز به کوچکترین تماسی با کنترل مورد نظر. برای مثال اگر مقدار یک خاصیت را تغییر دادید، بلافاصله بدون اینکه به کنترل مقید به آن اعلام کنیم که لطفا این مورد ویژه را برای من تغییر بده، شاهد نتیجه‌ی نهایی خواهیم بود.



اما استفاده‌ی پیشرفته از این لیست جنریک ویژه به همینجا ختم نشده و حین اضافه کردن کمی پیچیدگی به برنامه مشکلات عدیده‌ای بروز می‌کنند که آن‌ها را جهت دسترسی ساده‌ی بعدی در زیر لیست می‌کنم:

الف) اصلا Observable collection چیست؟ چکار می‌کند؟
List vs ObservableCollection vs INotifyPropertyChanged in Silverlight

ب) نمی‌توانم از این مجموعه‌ی اشیای خودآگاه سازنده در یک ترد استفاده کنم. مشکل کجاست؟
این روزها نمی‌توان یک برنامه‌ی دسکتاپ خوب را بدون استفاده از تردها متصور شد. اما به محض سعی در به روز رسانی این لیست جنریک در یک ترد دیگر (ترد دیگر منظور هر تردی بجز ترد اصلی برنامه است که کار مدیریت رابط کاربر را به عهده دارد) خطای زیر ظاهر می‌شود:
This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread

راه حل:
Adding to an ObservableCollection from a background thread

ج) یکی از خاصیت‌های یک شیء این لیست جنریک ویژه را تغییر داده‌ام. اما هیچ تغییری در کنترل بایند شده به آن مشاهده نمی‌کنم. مشکل در کجاست؟
راه حل: پیاده سازی اینترفیس INotifyPropertyChanged را فراموش کرده‌اید:
Data Binding in WPF with the Monostate Pattern

د) خوب، این که خیلی دردسر دارد! راه ساده‌تری برای تعریف این موارد نیست؟!
هوشمندانه‌ترین روشی که برای حل این مساله تابحال دیده‌ام:
An easier way to manage INotifyPropertyChanged

ه) زمانیکه این یک لیست جنریک خودآگاه سازنده را به یک مثلا listview بایند می‌کنم، دیگر نمی‌توانم با استفاده از متد clear items آن کنترل، نسبت به خالی کردن نمای ظاهری آن اقدام کنم. چکار باید کرد؟
خطای مشاهده شده:
Operation is not valid while ItemsSource is in use. Access and modify elements with ItemsControl.ItemsSource instead

راه حل:
همان Observable collection اصلی را تخلیه کنید، UI به صورت خودکار به روز خواهد شد.

و) اضافه کردن رنجی از اطلاعات به آن به صورتی یکباره ممکن است کند باشد. چه باید کرد؟
راه حل:
AddRange for ObservableCollection in Silverlight 3


نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 15 - بررسی تغییرات Caching
من یک چنین اکشن متدی دارم:
    [ResponseCache(Duration = 60)]
        public IActionResult GetTime()
        {
            return Content(DateTime.Now.ToString());
        }
و در پاسخ هم مقدار برابر است با:
cache-control: public,max-age=60
ولی با هر بار رفرش نتیجه به روز را میگیرم.برای همه بخش‌ها به همین شکل عمل میکند
مطالب
ASP.NET MVC #11

بررسی نکات تکمیلی Model binder در ASP.NET MVC

یک برنامه خالی جدید ASP.NET MVC را شروع کنید و سپس مدل زیر را به پوشه Models آن اضافه نمائید:

using System;

namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}

از این مدل چند مقصود ذیل دنبال می‌شوند:
استفاده از Id به عنوان primary key برای edit و update رکوردها. استفاده از DateTime برای اینکه اگر کاربری اطلاعات بی ربطی را وارد کرد چگونه باید این مشکل را در حالت model binding خودکار تشخیص داد و استفاده از IsAdmin برای یادآوری یک نکته امنیتی بسیار مهم که اگر حین model binding خودکار به آن توجه نشود، سایت را با مشکلات حاد امنیتی مواجه خواهد کرد. سیستم پیشرفته است. می‌تواند به صورت خودکار ورودی‌های کاربر را تبدیل به یک شیء حاضر و آماده کند ... اما باید حین استفاده از این قابلیت دلپذیر به یک سری نکات امنیتی هم دقت داشت تا سایت ما به نحو دلپذیری هک نشود!

در ادامه یک کنترلر جدید به نام UserController را به پوشه کنترلرهای پروژه اضافه نمائید. همچنین نام کنترلر پیش فرض تعریف شده در قسمت مسیریابی فایل Global.asax.cs را هم به User تغییر دهید تا در هربار اجرای برنامه در VS.NET، نیازی به تایپ آدرس‌های مرتبط با UserController نداشته باشیم.
یک منبع داده تشکیل شده در حافظه را هم برای نمایش لیستی از کاربران، به نحو زیر به پروژه اضافه خواهیم کرد:

using System;
using System.Collections.Generic;

namespace MvcApplication7.Models
{
public class Users
{
public IList<User> CreateInMemoryDataSource()
{
return new[]
{
new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
};
}
}
}

در اینجا فعلا هدف آشنایی با زیر ساخت‌های ASP.NET MVC است و درک صحیح نحوه کارکرد آن. مهم نیست از EF استفاده می‌کنید یا NH یا حتی ADO.NET کلاسیک و یا از Micro ORMهایی که پس از ارائه دات نت 4 مرسوم شده‌اند. تهیه یک ToList یا Insert و Update با این فریم ورک‌ها خارج از بحث جاری هستند.

سورس کامل کنترلر User به شرح زیر است:

using System;
using System.Linq;
using System.Web.Mvc;
using MvcApplication7.Models;

namespace MvcApplication7.Controllers
{
public class UserController : Controller
{
[HttpGet]
public ActionResult Index()
{
var usersList = new Users().CreateInMemoryDataSource();
return View(usersList); // Shows the Index view.
}

[HttpGet]
public ActionResult Details(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Details view.
}

[HttpGet]
public ActionResult Create()
{
var user = new User { AddDate = DateTime.Now };
return View(user); // Shows the Create view.
}

[HttpPost]
public ActionResult Create(User user)
{
if (this.ModelState.IsValid)
{
// todo: Add record
return RedirectToAction("Index");
}
return View(user); // Shows the Create view again.
}

[HttpGet]
public ActionResult Edit(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Edit view.
}

[HttpPost]
public ActionResult Edit(User user)
{
if (this.ModelState.IsValid)
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}

[HttpPost]
public ActionResult Delete(int id)
{
// todo: Delete record
return RedirectToAction("Index");
}
}
}

توضیحات:

ایجاد خودکار فرم‌های ورود اطلاعات

در قسمت قبل برای توضیح دادن نحوه ایجاد فرم‌ها در ASP.NET MVC و همچنین نحوه نگاشت اطلاعات آن‌ها به اکشن متدهای کنترلرها، فرم‌های مورد نظر را دستی ایجاد کردیم.
اما باید درنظر داشت که برای ایجاد Viewها می‌توان از ابزار توکار خود VS.NET نیز استفاده کرد و سپس اطلاعات و فرم‌های تولیدی را سفارشی نمود. این سریع‌ترین راه ممکن است زمانیکه مدل مورد استفاده کاملا مشخص است و می‌خواهیم Strongly typed views را ایجاد کنیم.
برای نمونه بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا گزینه‌ی create a strongly typed view را انتخاب کرده و سپس از لیست مدل‌ها، User را انتخاب نمائید. Scaffold template را هم بر روی حالت List قرار دهید.
برای متد Details هم به همین نحو عمل نمائید.
برای ایجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل یکی است. فقط Scaffold template انتخابی را بر روی Create قرار دهید تا فرم ورود اطلاعات، به صورت خودکار تولید شود.
متد Create در حالت HttpPost نیازی به View اضافی ندارد. چون صرفا قرار است اطلاعاتی را از سرور دریافت و ثبت کند.
برای ایجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با این تفاوت که Scaffold template انتخابی را بر روی گزینه Edit قرار دهید تا فرم ویرایش اطلاعات کاربر به صورت خودکار به پروژه اضافه شود.
متد Edit در حالت HttpPost نیازی به View اضافی ندارد و کارش تنها دریافت اطلاعات از سرور و به روز رسانی بانک اطلاعاتی است.
به همین ترتیب متد Delete نیز، نیازی به View خاصی ندارد. در اینجا بر اساس primary key دریافتی، می‌توان یک کاربر را یافته و حذف کرد.



سفارشی سازی Viewهای خودکار تولیدی

با کمک امکانات Scaffolding نامبرده شده، حجم قابل توجهی کد را در اندک زمانی می‌توان تولید کرد. بدیهی است حتما نیاز به سفارشی سازی کدهای تولیدی وجود خواهد داشت. مثلا شاید نیازی نباشد فیلد پسود کاربر، در حین نمایش لیست کاربران، نمایش داده شود. می‌شود کلا این ستون را حذف کرد و از این نوع مسایل.
یک مورد دیگر را هم در Viewهای تولیدی حتما نیاز است که ویرایش کنیم. آن هم مرتبط است به لینک حذف اطلاعات یک کاربر در صفحه Index.cshtml:

@Html.ActionLink("Delete", "Delete", new { id=item.Id }

در قسمت قبل هم عنوان شد که اعمال حذف باید بر اساس HttpPost محدود شوند تا بتوان میزان امنیت برنامه را بهبود داد. متد Delete هم در کنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراین ActionLink پیش فرض را حذف کرده و بجای آن فرم و دکمه زیر را قرار می‌دهیم تا اطلاعات به سرور Post شوند:

@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
<input type="submit" value="Delete"
onclick="return confirm ('Do you want to delete this record?');" />
}

در اینجا نحوه ایجاد یک فرم، که id رکورد متناظر را به سرور ارسال می‌کند، مشاهده می‌کنید.



علت وجود دو متد، به ازای هر Edit یا Create

به ازای هر کدام از متدهای Edit و Create دو متد HttpGet و HttpPost را ایجاد کرده‌ایم. کار متدهای HttpGet نمایش View‌های متناظر به کاربر هستند. بنابراین وجود آن‌ها ضروری است. در این حالت چون از دو Verb متفاوت استفاده شده، می‌توان متدهای هم نامی را بدون مشکل استفاده کرد. به هر کدام از افعال Get و Post و امثال آن، یک Http Verb گفته می‌شود.



بررسی معتبر بودن اطلاعات دریافتی

کلاس پایه Controller که کنترلرهای برنامه از آن مشتق می‌شوند، شامل یک سری خواص و متدهای توکار نیز هست. برای مثال توسط خاصیت this.ModelState.IsValid می‌توان بررسی کرد که آیا Model دریافتی معتبر است یا خیر. برای بررسی این مورد، یک breakpoint را بر روی سطر this.ModelState.IsValid در متد Create قرار دهید. سپس به صفحه ایجاد کاربر جدید مراجعه کرده و مثلا بجای تاریخ روز، abcd را وارد کنید. سپس فرم را به سرور ارسال نمائید. در این حالت مقدار خاصیت this.ModelState.IsValid مساوی false می‌باشد که حتما باید به آن پیش از ثبت اطلاعات دقت داشت.



شبیه سازی عملکرد ViewState در ASP.NET MVC

در متدهای Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شیء User دریافتی، به View بازگشت داده می‌شود. چرا؟
صفحات وب، زمانیکه به سرور ارسال می‌شوند، تمام اطلاعات کنترل‌های خود را از دست خواهد داد (صفحه پاک می‌شود، چون مجددا یک صفحه خالی از سرور دریافت خواهد شد). برای رفع این مشکل در ASP.NET Web forms، از مفهومی به نام ViewState کمک می‌گیرند. کار ViewState ذخیره موقت اطلاعات فرم جاری است برای استفاده مجدد پس از Postback. به این معنا که پس از ارسال فرم به سرور، اگر کاربری در textbox اول مقدار abc را وارد کرده بود، پس از نمایش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد کرد (شبیه سازی برنامه‌های دسکتاپ در محیط وب). بدیهی است وجود ViewState برای ذخیره سازی این نوع اطلاعات، حجم صفحه را بالا می‌برد (بسته به پیچیدگی صفحه ممکن است به چند صد کیلوبایت هم برسد).
در ASP.NET MVC بجای استفاده از ترفندی به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت می‌دهند. در این حالت پس از ارسال صفحه به سرور و نمایش مجدد صفحه ورود اطلاعات، تمام کنترل‌ها با همان مقادیر قبلی وارد شده توسط کاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed می‌باشد. در این حالت فریم ورک می‌داند که اطلاعات را چگونه به کنترل‌های قرار گرفته در صفحه نگاشت کند).
در مثال فوق، اگر اطلاعات وارد شده صحیح باشند، کاربر به صفحه Index هدایت خواهد شد. در غیراینصورت مجددا همان View جاری با همان اطلاعات model قبلی که کاربر تکمیل کرده است به او برای تصحیح، نمایش داده می‌شود. این مساله هم جهت بالا بردن سهولت کاربری برنامه بسیار مهم است. تصور کنید که یک فرم خالی با پیغام «تاریخ وارد شده معتبر نیست» مجدا به کاربر نمایش داده شود و از او درخواست کنیم که تمام اطلاعات دیگر را نیز از صفر وارد کند چون اطلاعات صفحه پس از ارسال به سرور پاک شده‌اند؛ که ... اصلا قابل قبول نیست و فوق‌العاده برنامه را غیرحرفه‌ای نمایش می‌دهد.



خطاهای نمایش داده شده به کاربر

به صورت پیش فرض خطایی که به کاربر نمایش داده می‌شود، استثنایی است که توسط فریم ورک صادر شده است. برای مثال نتوانسته است abcd را به یک تاریخ معتبر تبدیل کند. می‌توان توسط this.ModelState.AddModelError خطایی را نیز در اینجا اضافه کرد و پیغام بهتری را به کاربر نمایش داد. یا توسط یک سری data annotations هم کار اعتبار سنجی را سفارشی کرد که بحث آن به صورت جداگانه در یک قسمت مستقل بررسی خواهد شد.
ولی به صورت خلاصه اگر به فرم‌های تولید شده توسط VS.NET دقت کنید، در ابتدای هر فرم داریم:

@Html.ValidationSummary(true)

در اینجا خطاهای عمومی در سطح مدل نمایش داده می‌شوند. برای اضافه کردن این نوع خطاها، در متد AddModelError، مقدار key را خالی وارد کنید:

ModelState.AddModelError(string.Empty, "There is something wrong with model.");

همچنین در این فرم‌ها داریم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)

EditorFor سعی می‌کند اندکی هوش به خرج دهد. یعنی اگر خاصیت دریافتی مثلا از نوع bool بود، خودش یک checkbox را در صفحه نمایش می‌دهد. همچنین بر اساس متادیتا یک خاصیت نیز می‌تواند تصمیم گیری را انجام دهد. این متادیتا منظور attributes و data annotations ایی است که به خواص یک مدل اعمال می‌شود. مثلا اگر ویژگی HiddenInput را به یک خاصیت اعمال کنیم، به شکل یک فیلد مخفی در صفحه ظاهر خواهد شد.
یا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندنی نمایش می‌دهد. اصطلاحا به این نوع متدها، Templated Helpers هم گفته می‌شود. بحث بیشتر درباره‌ای این موارد به قسمتی مجزا و مستقل موکول می‌گردد. برای نمونه کل فرم ادیت برنامه را حذف کنید و بجای آن بنویسید Html.EditorForModel و سپس برنامه را اجرا کنید. یک فرم کامل خودکار ویرایش اطلاعات را مشاهده خواهید کرد (و البته نکات سفارشی سازی آن به یک قسمت کامل نیاز دارند).
در اینجا متد ValidationMessageFor کار نمایش خطاهای اعتبارسنجی مرتبط با یک خاصیت مشخص را انجام می‌دهد. بنابراین اگر قصد ارائه خطایی سفارشی و مخصوص یک فیلد مشخص را داشتید، در متد AddModelError، مقدار پارامتر اول یا همان key را مساوی نام خاصیت مورد نظر قرار دهید.


مقابله با مشکل امنیتی Mass Assignment در حین کار با Model binders

استفاده از Model binders بسیار لذت بخش است. یک شیء را به عنوان پارامتر اکشن متد خود معرفی می‌کنیم. فریم ورک هم در ادامه سعی می‌کند تا اطلاعات فرم را به خواص این شیء نگاشت کند. بدیهی است این روش نسبت به روش ASP.NET Web forms که باید به ازای تک تک کنترل‌های موجود در صفحه یکبار کار دریافت اطلاعات و مقدار دهی خواص یک شیء را انجام داد، بسیار ساده‌تر و سریعتر است.
اما اگر همین سیستم پیشرفته جدید ناآگاهانه مورد استفاده قرار گیرد می‌تواند منشاء حملات ناگواری شود که به نام «Mass Assignment» شهرت یافته‌اند.
همان صفحه ویرایش اطلاعات را درنظر بگیرید. چک باکس IsAdmin قرار است در قسمت مدیریتی برنامه تنظیم شود. اگر کاربری نیاز داشته باشد اطلاعات خودش را ویرایش کند، مثلا پسوردش را تغییر دهد، با یک صفحه ساده کلمه عبور قبلی را وارد کنید و دوبار کلمه عبور جدید را نیز وارد نمائید، مواجه خواهد شد. خوب ... اگر همین کاربر صفحه را جعل کند و فیلد چک باکس IsAdmin را به صفحه اضافه کند چه اتفاقی خواهد افتاد؟ بله ... مشکل هم همینجا است. در اینصورت کاربر عادی می‌تواند دسترسی خودش را تا سطح ادمین بالا ببرد، چون model binder اطلاعات IsAdmin را از کاربر دریافت کرده و به صورت خودکار به model ارائه شده، نگاشت کرده است.
برای مقابله با این نوع حملات چندین روش وجود دارند:
الف) ایجاد لیست سفید
به کمک ویژگی Bind می‌توان لیستی از خواص را جهت به روز رسانی به model binder معرفی کرد. مابقی ندید گرفته خواهند شد:

public ActionResult Edit([Bind(Include = "Name, Password")] User user)

در اینجا تنها خواص Name و Password توسط model binder به خواص شیء User نگاشت می‌شوند.
به علاوه همانطور که در قسمت قبل نیز ذکر شد، متد edit را به شکل زیر نیز می‌توان بازنویسی کرد. در اینجا متدهای توکار UpdateModel و TryUpdateModel نیز لیست سفید خواص مورد نظر را می‌پذیرند (اعمال دستی model binding):

[HttpPost]
public ActionResult Edit()
{
var user = new User();
if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}


ب) ایجاد لیست سیاه
به همین ترتیب می‌توان تنها خواصی را معرفی کرد که باید صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)

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

لازم به ذکر است که ویژگی Bind را به کل یک کلاس هم می‌توان اعمال کرد. برای مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication7.Models
{
[Bind(Exclude = "IsAdmin")]
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}

این مورد اثر سراسری داشته و قابل بازنویسی نیست. به عبارتی حتی اگر در متدی خاصیت IsAdmin را مجددا الحاق کنیم، تاثیری نخواهد داشت.
یا می‌توان از ویژگی ReadOnly هم استفاده کرد:
using System;
using System.ComponentModel;

namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }

[ReadOnly(true)]
public bool IsAdmin { set; get; }
}
}

در این حالت هم خاصیت IsAdmin هیچگاه توسط model binder به روز و مقدار دهی نخواهد شد.

ج) استفاده از ViewModels
این راه حلی است که بیشتر مورد توجه معماران نرم افزار است و البته کسانی که پیشتر با الگوی MVVM کار کرده باشند این نام برایشان آشنا است؛ اما در اینجا مفهوم متفاوتی دارد. در الگوی MVVM، کلاس‌های ViewModel شبیه به کنترلرها در MVC هستند یا به عبارتی همانند رهبر یک اکستر عمل می‌کنند. اما در الگوی MVC خیر. در اینجا فقط مدل یک View هستند و نه بیشتر. هدف هم این است که بین Domain Model و View Model تفاوت قائل شد.
کار View model در الگوی MVC، شکل دادن به چندین domain model و همچنین اطلاعات اضافی دیگری که نیاز هستند، جهت استفاده نهایی توسط یک View می‌باشد. به این ترتیب View با یک شیء سر و کار خواهد داشت و همچنین منطق شکل دهی به اطلاعات مورد نیازش هم از داخل View حذف شده و به خواص View model در زمان تشکیل آن منتقل می‌شود.
مشخصات یک View model خوب به شرح زیر است:
الف) رابطه بین یک View و View model آن، رابطه‌ای یک به یک است. به ازای هر View، بهتر است یک کلاس View model وجود داشته باشد.
ب) View ساختار View model را دیکته می‌کند و نه کنترلر.
ج) View modelها صرفا یک سری کلاس POCO (کلاس‌هایی تشکیل شده از خاصیت، خاصیت، خاصیت ....) هستند که هیچ منطقی در آن‌ها قرار نمی‌گیرد.
د) View model باید حاوی تمام اطلاعاتی باشد که View جهت رندر نیاز دارد و نه بیشتر و الزامی هم ندارد که این اطلاعات مستقیما به domain models مرتبط شوند. برای مثال اگر قرار است firstName+LastName در View نمایش داده شود، کار این جمع زدن باید حین تهیه View Model انجام شود و نه داخل View. یا اگر قرار است اطلاعات عددی با سه رقم جدا کننده به کاربر نمایش داده شوند، وظیفه View Model است که یک خاصیت اضافی را برای تهیه این مورد تدارک ببیند. یا مثلا اگر یک فرم ثبت نام داریم و در این فرم لیستی وجود دارد که تنها Id عنصر انتخابی آن در Model اصلی مورد استفاده قرار می‌گیرد، تهیه اطلاعات این لیست هم کار ViewModel است و نه اینکه مدام به Model اصلی بخواهیم خاصیت اضافه کنیم.

ViewModel چگونه پیاده سازی می‌شود؟
اکثر مقالات را که مطالعه کنید، این روش را توصیه می‌کنند:

public class MyViewModel
{
    public SomeDomainModel1 Model1 { get; set; }
    public SomeDomainModel2 Model2 { get; set; }
    ...
}

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

public class MyViewModel
{
    public string SomeExtraField1 { get; set; }
    public string SomeExtraField2 { get; set; }
public IEnumerable<SelectListItem> StateSelectList { get; set; }
// ...
    public string PersonFullName { set; set; }
}

در اینجا، View متناظری، قرار است نام کامل یک شخص را به علاوه یک سری اطلاعات اضافی که در domain model نیست، نمایش دهد. مثلا نمایش نام استان‌ها که نهایتا Id انتخابی آن قرار است در برنامه استفاده شود.
خلاصه علت وجودی ViewModel این موارد است:
الف) Model برنامه را مستقیما در معرض استفاده قرار ندهیم (عدم رعایت این نکته به مشکلات امنیتی حادی هم حین به روز رسانی اطلاعات ممکن است ختم ‌شود که پیشتر توضیح داده شد).
ب) فیلدهای نمایشی اضافی مورد نیاز یک View را داخل Model برنامه تعریف نکنیم (مثلا تعاریف عناصر یک دراپ داون لیست، جایش اینجا نیست. مدل فقط نیاز به Id عنصر انتخابی آن دارد).

با این توضیحات، اگر View به روز رسانی اطلاعات کلمه عبور کاربر، تنها به اطلاعات id آن کاربر و کلمه عبور او نیاز دارد، فقط باید همین اطلاعات را در اختیار View قرار داد و نه بیشتر:

namespace MvcApplication7.Models
{
public class UserViewModel
{
public int Id { set; get; }
public string Password { set; get; }
}
}

به این ترتیب دیگر خاصیت IsAdming اضافه‌ای وجود ندارد که بخواهد مورد حمله واقع شود.



استفاده از model binding برای آپلود فایل به سرور

برای آپلود فایل به سرور تنها کافی است یک اکشن متد به شکل زیر را تعریف کنیم. HttpPostedFileBase نیز یکی دیگر از model binderهای توکار ASP.NET MVC است:

[HttpGet]
public ActionResult Upload()
{
return View(); // Shows the upload page
}

[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
string filename = Server.MapPath("~/files/somename.ext");
file.SaveAs(filename);
return RedirectToAction("Index");
}

View متناظر هم می‌تواند به شکل زیر باشد:

@{
ViewBag.Title = "Upload";
}
<h2>
Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User",
method: FormMethod.Post,
htmlAttributes: new { enctype = "multipart/form-data" }))
{
<text>Upload a photo:</text> <input type="file" name="photo" />
<input type="submit" value="Upload" />
}

اگر دقت کرده باشید در طراحی ASP.NET MVC از anonymously typed objects زیاد استفاده می‌شود. در اینجا هم برای معرفی enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتی هر جایی که مشخص نبوده چه تعداد ویژگی یا کلا چه ویژگی‌ها و خاصیت‌هایی را می‌توان تنظیم کرد، اجازه تعریف آن‌ها را به صورت anonymously typed objects میسر کرده‌اند. یک نمونه دیگر آن در متد routes.MapRoute فایل Global.asax.cs است که پارامتر سوم دریافت مقدار پیش فرض‌ها نیز anonymously typed object است. یا نمونه دیگر آن‌را در همین قسمت در جایی که لینک delete را به فرم تبدیل کردیم مشاهده نمودید. مقدار routeValues هم یک anonymously typed object معرفی شد.



سفارشی سازی model binder پیش فرض ASP.NET MVC

در همین مثال فرض کنید تاریخ را به صورت شمسی از کاربر دریافت می‌کنیم. خاصیت تعریف شده هم DateTime میلادی است. به عبارتی model binder حین تبدیل رشته تاریخ شمسی دریافتی به تاریخ میلادی با شکست مواجه شده و نهایتا خاصیت this.ModelState.IsValid مقدارش false خواهد بود. برای حل این مشکل چکار باید کرد؟
برای این منظور باید نحوه پردازش یک نوع خاص را سفارشی کرد. ابتدا با پیاده سازی اینترفیس IModelBinder شروع می‌کنیم. توسط bindingContext.ValueProvider می‌توان به مقداری که کاربر وارد کرده در میانه راه دسترسی یافت. آن‌را تبدیل کرده و نمونه صحیح را بازگشت داد.
نمونه‌ای از این پیاده سازی را در ادامه ملاحظه می‌کنید:

using System;
using System.Globalization;
using System.Web.Mvc;

namespace MvcApplication7.Binders
{
public class PersianDateModelBinder : IModelBinder
{

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
if (parts.Length != 3) return null;
int year = int.Parse(parts[0]);
int month = int.Parse(parts[1]);
int day = int.Parse(parts[2]);
actualValue = new DateTime(year, month, day, new PersianCalendar());
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}

bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}

سپس برای معرفی PersianDateModelBinder جدید تنها کافی است سطر زیر را

ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());

به متد Application_Start قرار گرفته در فایل Global.asax.cs برنامه اضافه کرد. از این پس کاربران می‌توانند تاریخ‌ها را در برنامه شمسی وارد کنند و model binder بدون مشکل خواهد توانست اطلاعات ورودی را به معادل DateTime میلادی آن تبدیل کند و استفاده نماید.
تعریف مدل بایندر سفارشی در فایل Global.asax.cs آن‌را به صورت سراسری در تمام مدل‌ها و اکشن‌متدها فعال خواهد کرد. اگر نیاز بود تنها یک اکشن متد خاص از این مدل بایندر سفارشی استفاده کند می‌توان به روش زیر عمل کرد:

public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)

همچنین ویژگی ModelBinder را به یک کلاس هم می‌توان اعمال کرد:

[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{


نظرات مطالب
کنترل DatePicker شمسی مخصوص Silverlight 4
چندتا مطلب هست:
- احتمالا منظور شما از VB.NET همان WPF بوده و این بحث جاری هم سیلورلایت است. نسخه WPF این کنترل هم موجود است. باید سورس‌ها را دریافت کنید و کامپایل. مثال WPF هم دارد در سورس‌های مجموعه (در پوشه WpfPersianDatePickerUsage).
- خطایی که نوشتید مشکل VS.NET است که در دو جای مختلف به دنبال یک سری اسمبلی می‌گردد. در مسیر ProjectAssemblies\zgrq7cfx01 و در مسیر Common7\IDE. این تداخل رو می‌تونید با پاک کردن پوشه‌های obj و bin و مسیرهای یاد شده و بعد rebuild کامل پروژه احتمالا برطرف کنید. در کل مشکل کنترل دریافتی نیست. تداخل نگارش‌ها رخ داده روی سیستم شما.
- بله. مراجعه کنید به مثال موجود در پوشه WpfPersianDatePickerUsage. (سورس را باید دریافت کنید)
بازخوردهای پروژه‌ها
تغییر نام یک فایل
با سلام مجدد
در هنگام تغییر نام فایل در قسمتی که مسیر فایل نمایش داده شده هنگامی که بر روی نام فایل کلیک می‌کنیم یه خطای کنترل نشده نمایش داده می‌شود

 The filename, directory name, or volume label syntax is incorrect.
لطفا بررسی فرمایید
تشکر
مطالب
خلاصه‌ای از مبحث نمایش اطلاعات hierarchical در WPF

در این مطلب خلاصه‌ای را در مورد نحوه‌ی نمایش اطلاعات hierarchical (سلسله مراتبی، درختی) در WPF به همراه یک سری لینک مرتبط ملاحظه خواهید نمود.

کلاس زیر را در نظر بگیرید:
using System.Collections.Generic;

namespace WpfTests.Hierarchy.Raw.Model
{
public class Person
{
private readonly List<Person> _children = new List<Person>();
public IList<Person> Children
{
get { return _children; }
}

public string Name { get; set; }
}
}
و همچنین یک ObservableCollection ساخته شده از آن‌را با مقدار دهی اولیه:
using System.Collections.ObjectModel;

namespace WpfTests.Hierarchy.Raw.Model
{
public class People : ObservableCollection<Person>
{
public People()
{
this.Add(
new Person
{
Name = "P1",
Children =
{
new Person
{
Name="P2",
Children=
{
new Person
{
Name="P3",
Children=
{
new Person
{
Name="P4",
}
}
}
}
}
}
}
);
}
}
}
قصد داریم این اطلاعات را در یک TreeView نمایش دهیم.
روش صحیح Binding این نوع اطلاعات در WPF استفاده از HierarchicalDataTemplate است به صورت زیر :
<TreeView ItemsSource="{Binding People}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>


یک سری منبع آموزشی برای آشنایی بیشتر با HierarchicalDataTemplate
Hierarchical Databinding in WPF
Binding WPF Treeview and Objects
A TreeView, a HierarchicalDataTemplate, and a 2D collection
Non-recursive WPF TreeView controls

همچنین هنگام کار با بانک‌های اطلاعاتی:
- یک Extension method عالی قابل استفاده در LINQ to SQL و همچنین Entity framework به نام AsHierarchy
- مثالی دیگر از کاربرد LINQ to SQL برای این منظور
- و یا مثالی از ADO.NET و DataSets و مثالی دیگر

مطالب
کش کردن اطلاعات غیر پویا در ASP.Net - قسمت چهارم

قسمت‌های اول تا سوم این مقاله: + و + و +

در قسمت چهارم قصد داریم هدر مربوط به Content Expiration Date را توسط یک Http module به محتوای غیرپویای سایت مانند تصاویر ، فایل‌های CSS و غیره اعمال کنیم. این روش از روش قسمت دوم ساده‌تر است و جامع‌تر.
ابتدا یک پروژه‌ی Class library جدید را به نام StaticContentCacheModule ایجاد کرده و سپس ارجاعی را به اسمبلی استاندارد System.Web.dll به آن خواهیم افزود. سپس کدهای مرتبط با این ماژول به شرح زیر هستند:

//StaticCache .cs
using System;
using System.Web;

namespace StaticContentCacheModule
{
public class StaticCache : IHttpModule
{
public void Init(HttpApplication context)
{
context.PreSendRequestHeaders += context_PreSendRequestHeaders;
}

static void context_PreSendRequestHeaders(object sender, EventArgs e)
{
//capture the current Response
var currentResponse = ((HttpApplication)sender).Response;

if (CacheManager.ShouldCache(currentResponse.ContentType))
{
currentResponse.AddHeader("cache-control", "public");
currentResponse.AddHeader("Expires", DateTime.Now.Add(TimeSpan.FromDays(30)).ToString());
}
}

public void Dispose() { }
}
}

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

//CacheManager.cs
using System;

namespace StaticContentCacheModule
{
class CacheManager
{
public static bool ShouldCache(string contentType)
{
contentType = contentType.ToLower();
string[] parts =
contentType.Split(
new[] { ';' },
StringSplitOptions.RemoveEmptyEntries
);

if (parts.Length > 0)
contentType = parts[0];

bool cache = false;

switch (contentType)
{
case "text/css":
cache = true; break;
case "text/javascript":
case "text/jscript":
cache = true; break;
case "image/jpeg":
cache = true; break;
case "image/gif":
cache = true; break;
case "application/octet-stream":
cache = true; break;
default:
{
if (contentType.Contains("javascript"))
cache = true;
if (contentType.Contains("css"))
cache = true;
if (contentType.Contains("image"))
cache = true;
if (contentType.Contains("application"))
cache = true;
}
break;
}

return cache;
}
}
}

در این کلاس، contentType دریافتی بررسی می‌شود. اگر نوع محتوای قابل ارائه از نوع CSS ، JavaScript ، تصویر و یا Application بود، یک مقدار true بازگشت داده خواهد شد.
نهایتا برای استفاده از این Http module جدید در IIS6 به قبل در وب کانفیگ برنامه خواهیم داشت:

<httpModules>
<add name="StaticContentCacheModule" type="StaticContentCacheModule.StaticCache, StaticContentCacheModule"/>
</httpModules>

و یا در IIS7 این تغییرات به صورت زیر می‌تواند باشد:

<system.webServer>
<modules>
<add name="StaticContentCacheModule" type="StaticContentCacheModule.StaticCache, StaticContentCacheModule"/>
</modules>

اکنون اگر یک پروژه‌ی آزمایشی جدید ASP.Net را گشوده و فایل css ساده‌ای را به آن اضافه کنیم، بررسی هدر نهایی توسط افزونه‌ی YSlow به صورت زیر خواهد بود:



مطالب
Blazor 5x - قسمت 32 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 2 - ثبت نام،‌ ورود به سیستم و خروج از آن
در قسمت 25، سرویس‌های سمت سرور اعتبارسنجی و احراز هویت مبتنی بر ASP.NET Core Identity را تهیه کردیم. همچنین در قسمت قبل، سرویس‌های سمت کلاینت کار با این Web API Endpoints را توسعه دادیم. در این مطلب، رابط کاربری متصل کننده‌ی بخش‌های سمت کلاینت و سمت سرور را تکمیل خواهیم کرد.


تکمیل فرم ثبت نام کاربران


در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده می‌کنید:
@page "/registration"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager


<EditForm Model="UserForRegistration" OnValidSubmit="RegisterUser" class="pt-4">
    <DataAnnotationsValidator />
    <div class="py-4">
        <div class=" row form-group ">
            <div class="col-6 offset-3 ">
                <div class="card border">
                    <div class="card-body px-lg-5 pt-4">
                        <h3 class="col-12 text-success text-center py-2">
                            <strong>Sign Up</strong>
                        </h3>
                        @if (ShowRegistrationErrors)
                        {
                            <div>
                                @foreach (var error in Errors)
                                {
                                    <p class="text-danger text-center">@error</p>
                                }
                            </div>
                        }
                        <hr style="background-color:aliceblue" />
                        <div class="py-2">
                            <InputText @bind-Value="UserForRegistration.Name" class="form-control" placeholder="Name..." />
                            <ValidationMessage For="(()=>UserForRegistration.Name)" />
                        </div>
                        <div class="py-2">
                            <InputText @bind-Value="UserForRegistration.Email" class="form-control" placeholder="Email..." />
                            <ValidationMessage For="(()=>UserForRegistration.Email)" />
                        </div>
                        <div class="py-2 input-group">
                            <div class="input-group-prepend">
                                <span class="input-group-text"> +1</span>
                            </div>
                            <InputText @bind-Value="UserForRegistration.PhoneNo" class="form-control" placeholder="Phone number..." />
                            <ValidationMessage For="(()=>UserForRegistration.PhoneNo)" />
                        </div>
                        <div class="form-row py-2">
                            <div class="col">
                                <InputText @bind-Value="UserForRegistration.Password" type="password" id="password" placeholder="Password..." class="form-control" />
                                <ValidationMessage For="(()=>UserForRegistration.Password)" />
                            </div>
                            <div class="col">
                                <InputText @bind-Value="UserForRegistration.ConfirmPassword" type="password" id="confirm" class="form-control" placeholder="Confirm Password..." />
                                <ValidationMessage For="(()=>UserForRegistration.ConfirmPassword)" />
                            </div>
                        </div>
                        <hr style="background-color:aliceblue" />
                        <div class="py-2">
                            @if (IsProcessing)
                            {
                                <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button>
                            }
                            else
                            {
                                <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Register</button>
                            }
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</EditForm>

@code{
    UserRequestDTO UserForRegistration = new UserRequestDTO();
    bool IsProcessing;
    bool ShowRegistrationErrors;
    IEnumerable<string> Errors;

    private async Task RegisterUser()
    {
        ShowRegistrationErrors = false;
        IsProcessing = true;
        var result = await AuthenticationService.RegisterUserAsync(UserForRegistration);
        if (result.IsRegistrationSuccessful)
        {
            IsProcessing = false;
            NavigationManager.NavigateTo("/login");
        }
        else
        {
            IsProcessing = false;
            Errors = result.Errors;
            ShowRegistrationErrors = true;
        }
    }
}
توضیحات:
- مدل این فرم بر اساس UserRequestDTO تشکیل شده‌است که همان شیءای است که اکشن متد ثبت نام سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آن‌را در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال می‌شود.
- در اینجا روشی را جهت غیرفعال کردن یک دکمه، پس از کلیک بر روی آن مشاهده می‌کنید. می‌توان پس از کلیک بر روی دکمه‌ی ثبت نام، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمه‌ی جاری را برای مثال با ویژگی disabled در صفحه درج کرد و یا حتی آن‌را از صفحه حذف کرد. این روش، یکی از روش‌های جلوگیری از کلیک چندباره‌ی کاربر، بر روی یک دکمه‌است.
- فرم جاری، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش می‌دهد که حاصل از ارسال آن‌ها توسط اکشن متد ثبت نام است:


- پس از پایان موفقیت آمیز ثبت نام، کاربر را به سمت فرم لاگین هدایت می‌کنیم.


تکمیل فرم ورود به سیستم کاربران


در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده می‌کنید:
@page "/login"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

<div id="logreg-forms">
    <h1 class="h3 mb-3 pt-3 font-weight-normal text-primary" style="text-align:center;">Sign In</h1>
    <EditForm Model="UserForAuthentication" OnValidSubmit="LoginUser">
        <DataAnnotationsValidator />
        @if (ShowAuthenticationErrors)
        {
            <p class="text-center text-danger">@Errors</p>
        }
        <InputText @bind-Value="UserForAuthentication.UserName" id="email" placeholder="Email..." class="form-control mb-2" />
        <ValidationMessage For="(()=>UserForAuthentication.UserName)"></ValidationMessage>
        <InputText @bind-Value="UserForAuthentication.Password" type="password" placeholder="Password..." id="password" class="form-control mb-2" />
        <ValidationMessage For="(()=>UserForAuthentication.Password)"></ValidationMessage>
        @if (IsProcessing)
        {
            <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button>
        }
        else
        {
            <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Sign in</button>
        }
        <a href="/registration" class="btn btn-primary text-white mt-3"><i class="fas fa-user-plus"></i> Register as a new user</a>
    </EditForm>
</div>
@code
{
    AuthenticationDTO UserForAuthentication = new AuthenticationDTO();
    bool IsProcessing = false;
    bool ShowAuthenticationErrors;
    string Errors;
    string ReturnUrl;

    private async Task LoginUser()
    {
        ShowAuthenticationErrors = false;
        IsProcessing = true;
        var result = await AuthenticationService.LoginAsync(UserForAuthentication);
        if (result.IsAuthSuccessful)
        {
            IsProcessing = false;
            var absoluteUri = new Uri(NavigationManager.Uri);
            var queryParam = HttpUtility.ParseQueryString(absoluteUri.Query);
            ReturnUrl = queryParam["returnUrl"];
            if (string.IsNullOrEmpty(ReturnUrl))
            {
                NavigationManager.NavigateTo("/");
            }
            else
            {
                NavigationManager.NavigateTo("/" + ReturnUrl);
            }
        }
        else
        {
            IsProcessing = false;
            Errors = result.ErrorMessage;
            ShowAuthenticationErrors = true;
        }
    }
}
توضیحات:
- مدل این فرم بر اساس AuthenticationDTO تشکیل شده‌است که همان شیءای است که اکشن متد لاگین سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آن‌را در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال می‌شود.
- در اینجا نیز همانند فرم ثبت نام، پس از کلیک بر روی دکمه‌ی ورود به سیستم، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمه‌ی جاری را با ویژگی disabled در صفحه درج کرد‌ه‌ایم تا از کلیک چندباره‌ی کاربر، جلوگیری شود.
- این فرم، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش می‌دهد که حاصل از ارسال آن‌ها توسط اکشن متد لاگین است:


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


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


تغییر منوی راهبری سایت، بر اساس وضعیت لاگین شخص


تا اینجا قسمت‌های ثبت نام و ورود به سیستم را تکمیل کردیم. در ادامه نیاز داریم تا منوی سایت را هم بر اساس وضعیت اعتبارسنجی شخص، تغییر دهیم. برای مثال اگر شخصی به سیستم وارد شده‌است، باید در منوی سایت، لینک خروج و نام خودش را مشاهده کند و نه مجددا لینک‌های ثبت‌نام و لاگین را. جهت تغییر منوی راهبری سایت، کامپوننت Shared\NavMenu.razor را گشوده و لینک‌های قبلی ثبت‌نام و لاگین را با محتوای زیر جایگزین می‌کنیم:
<AuthorizeView>
    <Authorized>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="#">
             <span class="p-2">
                Hello, @context.User.Identity.Name!
             </span>
          </NavLink>
        </li>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="logout">
             <span class="p-2">
                Logout
             </span>
          </NavLink>
        </li>
    </Authorized>
    <NotAuthorized>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="registration">
            <span class="p-2">
               Register
            </span>
          </NavLink>
        </li>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="login">
            <span class="p-2">
              Login
            </span>
          </NavLink>
        </li>
    </NotAuthorized>
</AuthorizeView>
نمونه‌ی چنین منویی را در قسمت 22 نیز مشاهده کرده بودید. AuthorizeView، یکی از کامپوننت‌های استانداردBlazor  است. زمانیکه کاربری به سیستم لاگین کرده باشد، فرگمنت Authorized و در غیر اینصورت قسمت NotAuthorized آن‌را مشاهده خواهید کرد و همانطور که در قسمت قبل نیز عنوان شد، این کامپوننت برای اینکه کار کند، نیاز دارد به اطلاعات AuthenticationState (و یا همان لیستی از User Claims) دسترسی داشته باشد که آن‌را توسط یک AuthenticationStateProvider سفارشی، به سیستم معرفی و توسط کامپوننت CascadingAuthenticationState، به صورت آبشاری در اختیار تمام کامپوننت‌های برنامه قرار دادیم که نمونه‌ای از آن، درج مقدار context.User.Identity.Name در منوی سایت است.


تکمیل قسمت خروج از سیستم

اکنون که لینک logout را در منوی سایت، پس از ورود به سیستم نمایش می‌دهیم، می‌توان کدهای کامپوننت آن‌را (Pages\Authentication\Logout.razor) به صورت زیر تکمیل کرد:
@page "/logout"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

@code
{
    protected async override Task OnInitializedAsync()
    {
        await AuthenticationService.LogoutAsync();
        NavigationManager.NavigateTo("/");
    }
}
در اینجا در ابتدا توسط سرویس IClientAuthenticationService و متد LogoutAsync آن، کلیدهای Local Storage مربوط به JWT و اطلاعات کاربر حذف می‌شوند و سپس کاربر به صفحه‌ی اصلی هدایت خواهد شد.

مشکل! پس از کلیک بر روی logout، هرچند می‌توان مشاهده کرد که اطلاعات Local Storage به درستی حذف شده‌اند، اما ... پس از هدایت به صفحه‌ی اصلی برنامه، هنوز هم لینک logout و نام کاربری شخص نمایان هستند و به نظر هیچ اتفاقی رخ نداده‌است!
علت اینجا است که AuthenticationStateProvider سفارشی را که تهیه کردیم، فاقد اطلاع رسانی تغییر وضعیت است:
namespace BlazorWasm.Client.Services
{
    public class AuthStateProvider : AuthenticationStateProvider
    {
        // ...

        public void NotifyUserLoggedIn(string token)
        {
            var authenticatedUser = new ClaimsPrincipal(
                                        new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                                    );
            var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
            base.NotifyAuthenticationStateChanged(authState);
        }

        public void NotifyUserLogout()
        {
            var authState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
            base.NotifyAuthenticationStateChanged(authState);
        }
    }
}
در اینجا نیاز است پس از لاگین و یا خروج شخص، حتما متد NotifyAuthenticationStateChanged کلاس پایه‌ی AuthenticationStateProvider فراخوانی شود تا وضعیت AuthenticationState ای که در اختیار کامپوننت‌ها قرار می‌گیرد نیز تغییر کند. در غیراینصورت login و logout و یا هر تغییری در لیست user claims، به صورت آبشاری در اختیار کامپوننت‌های برنامه قرار نمی‌گیرند. به همین جهت دو متد عمومی NotifyUserLoggedIn و NotifyUserLogout را به AuthStateProvider اضافه می‌کنیم، تا این متدها را در زمان‌های لاگین و خروج از سیستم، در سرویس ClientAuthenticationService، فراخوانی کنیم:
namespace BlazorWasm.Client.Services
{
    public class ClientAuthenticationService : IClientAuthenticationService
    {
        private readonly HttpClient _client;
        private readonly ILocalStorageService _localStorage;
        private readonly AuthenticationStateProvider _authStateProvider;

        public ClientAuthenticationService(
            HttpClient client,
            ILocalStorageService localStorage,
            AuthenticationStateProvider authStateProvider)
        {
            _client = client;
            _localStorage = localStorage;
            _authStateProvider = authStateProvider;
        }

        public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication)
        {
            // ...
            if (response.IsSuccessStatusCode)
            {
                //...
                ((AuthStateProvider)_authStateProvider).NotifyUserLoggedIn(result.Token);

                return new AuthenticationResponseDTO { IsAuthSuccessful = true };
            }
            //...
        }

        public async Task LogoutAsync()
        {
            //...
            ((AuthStateProvider)_authStateProvider).NotifyUserLogout();
        }
    }
}
در اینجا تغییرات لازم اعمالی به سرویس ClientAuthenticationService را مشاهده می‌کنید:
- ابتدا AuthenticationStateProvider به سازنده‌ی کلاس تزریق شده‌است.
- سپس در حین لاگین موفق، متد NotifyUserLoggedIn آن فراخوانی شده‌است.
- در آخر پس از خروج از سیستم، متد NotifyUserLogout فراخوانی شده‌است.

پس از این تغییرات اگر بر روی لینک logout کلیک کنیم، این گزینه به درستی عمل کرده و اینبار شاهد نمایش مجدد لینک‌های لاگین و ثبت نام خواهیم بود.


محدود کردن دسترسی به صفحات برنامه بر اساس نقش‌های کاربران

پس از ورود کاربر به سیستم و تامین AuthenticationState، اکنون می‌خواهیم تنها این نوع کاربران اعتبارسنجی شده بتوانند جزئیات اتاق‌ها (برای شروع رزرو) و یا صفحه‌ی نمایش نتیجه‌ی پرداخت را مشاهده کنند. البته نمی‌خواهیم صفحه‌ی نمایش لیست اتاق‌ها را محدود کنیم. برای این منظور ویژگی Authorize را به ابتدای تعاریف کامپوننت‌های PaymentResult.razor و RoomDetails.razor، اضافه می‌کنیم:
@attribute [Authorize(Roles = ‍ConstantRoles.Customer)]
که البته در اینجا ذکر فضای نام آن در فایل BlazorWasm.Client\_Imports.razor، ضروری است:
@using Microsoft.AspNetCore.Authorization

با این تعریف، دسترسی به صفحات کامپوننت‌های یاد شده، محدود به کاربرانی می‌شود که دارای نقش Customer هستند. برای معرفی بیش از یک نقش، فقط کافی است لیست نقش‌های مدنظر را که می‌توانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننت‌ها معرفی کرد و نمونه‌ای از آن‌را در مطلب 23 مشاهده کردید.
نکته‌ی مهم: فیلتر Authorize را باید بر روی اکشن متدهای متناظر سمت سرور نیز قرار داد؛ در غیراینصورت تنها نیمی از کار انجام شده‌است و هنوز آزادانه می‌توان با Web API Endpoints موجود کار کرد.


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

پیشتر در فرم RoomDetails.razor، اطلاعات ابتدایی کاربر را مانند نام او، دریافت می‌کردیم. اکنون با توجه به محدود شدن این کامپوننت به کاربران لاگین کرده، می‌توان اطلاعات کاربر وارد شده‌ی به سیستم را نیز به صورت خودکار بارگذاری و تکمیل کرد:
@page "/hotel-room-details/{Id:int}"

// ...

@code {
     // ...

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                // ...

                if (await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails) != null)
                {
                    var userInfo = await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails);
                    HotelBooking.OrderDetails.UserId = userInfo.Id;
                    HotelBooking.OrderDetails.Name = userInfo.Name;
                    HotelBooking.OrderDetails.Email = userInfo.Email;
                    HotelBooking.OrderDetails.Phone = userInfo.PhoneNo;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
در اینجا با توجه به اینکه UserId هم مقدار دهی می‌شود، می‌توان سطر زیر را از ابتدای متد SaveRoomOrderDetailsAsync سرویس ClientRoomOrderDetailsService، حذف کرد:
public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
{
   // details.UserId = "unknown user!";
به این ترتیب هویت کاربری که کار خرید را انجام می‌دهد، دقیقا مشخص خواهد شد و همچنین پس از بازگشت از صفحه‌ی پرداخت بانکی، اطلاعات او مجددا از Local Storage دریافت و مقدار دهی اولیه می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-32.zip
مطالب
WF:Windows Workflow #2
ایجاد یک گردش ساده

در این دوره آموزشی قصد آموزش WF4  را داریم. برای ایجاد یک پروژه از نوع WF4  نیاز به VS2010 یا VS2012 است.
زمانیکه ویژوال استودیو را باز می‌کنید و بر روی گزینه ایجاد پروژه جدید کلیک می‌نمائید، در قسمت Workflow، چندین نوع پروژه وجود دارد که هر کدام از آن‌ها را به نوبت بررسی خواهیم کرد.
ابتدا یک پروژه از نوع Workflow Console Application  را ایجاد کنید:

پس ایجاد پروژه، اگر دقت داشته باشید، فایل ایجاد شده با پسوند .XAML  می‌باشد و یک کلاس Program.cs  هم در پروژه ایجاد گردیده که حاوی کلاس Main است و کار آن دقیقا مثل برنامه‌های کنسول معمولی می‌باشد.
اگر به قسمت پایین ویژوال استودیو دقت کنید، سه گزینه به نام‌های Variables ,Arguments ,Imports قابل مشاهده هستند:
  1. Variables : همین طور که از نام این گزینه بر می‌آید، برای تعریف متغیر‌ها در طول یک فرآیند می‌باشد.
  2. Arguments : این گزینه هم مانند قسمت قبل عمل کرده، ولی تفاوت‌هایی باهم دارند. به عنوان مثال در گزینه ۱ می‌توان برای متغیر، محدوده ایجاد کرد. ولی در گزینه 2 هیچگونه محدودیتی وجود ندارد و در تمام  Workflowها می‌توان از آن استفاده کرد و دیگر اینکه در گزینه ۲ می‌توان به متغیر گفت که از نوع ورودی In یا از نوع خروجی Out ویا هر دو  In/Out و یا اینکه از نوع Property باشد.
  3. Imports : این قسمت فضا‌های نامی که در برنامه استفاده می شوند، نشان داده می‌شوند .

برای ایجاد یک Flow ابتدا باید از  Toolbox، قسمت Control Flow، کنترل  Sequence را به داخل صفحه کشید و  پس از آن می‌توانیم از سایر کنترل‌ها استفاده نمائیم. پس از این کار، از قسمت  Primitives کنترل  WriteLine را به درون Sequence انتقال می‌دهیم .


این کنترل برای چاپ مقادیر در خروجی می‌باشد. مانند کدی که در زیر نوشته شده است عمل کرده است و این کار را از طریق خاصیتی به نام Text انجام می‌دهد.
 Console.WriteLine("Hello Word");
نکته: توجه کنید برای تغییر نام اجزایی که به درون صفحه کشیده می‌شوند، می‌توان برروی نام آن دوبار کلیک کرده و نام آن را تغییر دهید و یا با انتخاب آن، در پنجره Properties نام آن را عوض کنیم. برای این کار کافی است مقدار گزینه DisplayName  را با مقدار مورد نظر عوض نمائیم. این گزینه در تمام اجزای Workflow  موجود می‌باشد.
حال باید برنامه را اجرا کنیم تا خروجی را که چاپ رشته  "Hello Word"  است، مشاهده نمائیم. ولی قبل از آن باید تغییراتی در فایل Program.cs داده شود که به شرح زیر می‌باشد:
WorkflowInvoker.Invoke( new Workflow1());

 Console.WriteLine("Press ENTER to exit"); 
 Console.ReadLine();
از کلاس  WorkflowInvoker برای اجرای Flow مورد نظر استفاده شده و این‌کار از طریق متد Invoke انجام داده می‌شود.