بازخوردهای دوره
بوت استرپ (نگارش 3) چیست؟
سلام
میخواستم بدونم چرا به جای خواندن جی کوئری از سایت آن
 < script src = "http://code.jquery.com/jquery-latest.min.js" ></ script >
اون رو هم مثل بقیه فایل‌های جاوا اسکریپت دانلود نمیکنیم تا به صورت local استفاده کنیم؟
 < script src = "Scripts/jquery-latest.min.js  " ></ script >
مطالب
آشنایی با mocking frameworks - قسمت دوم

استفاده از mocking frameworks :

تعدادی از چارچوب‌های تقلید نوشته شده برای دات نت فریم ورک مطابق لیست زیر بوده و هدف از آن‌ها ایجاد ساده‌تر اشیاء تقلید برای ما می‌باشد:

Nmock : http://www.nmock.org
Moq : http://code.google.com/p/moq
Rhino Mocks : http://ayende.com/projects/rhino-mocks.aspx
TypeMock : http://www.typemock.com
EasyMock.Net : http://sourceforge.net/projects/easymocknet

در این بین Rhino Mocks که توسط یکی از اعضای اصلی تیم NHibernate به وجود آمده است، در مجامع مرتبط بیشتر مورد توجه است. برای آشنایی بیشتر با آن می‌توان به این ویدیوی رایگان آموزشی در مورد آن مراجعه نمود (حدود یک ساعت است).



خلاصه‌ا‌ی در مورد نحوه‌ی استفاده از Rhino Mocks :
پس از دریافت کتابخانه سورس باز Rhino Mocks ، ارجاعی را به اسمبلی Rhino.Mocks.dll آن، در پروژه آزمون واحد خود اضافه نمائید.
یک Rhino mock test با ایجاد شیءایی از MockRepository شروع می‌شود و کلا از سه قسمت تشکیل می‌گردد:
الف) ایجاد شیء Mock یا Arrange . هدف از ایجاد شیء mock ، جایگزین کردن و یا تقلید یک شیء واقعی جهت مباحثی مانند ایزوله سازی آزمایشات، بالابردن سرعت آن‌ها و متکی به خود کردن این آزمایشات می‌باشد. همچنین در این حالت نتایج false positive نیز کاهش می‌یابند. منظور از نتایج false positive این است که آزمایش باید با موفقیت به پایان برسد اما اینگونه نشده و علت آن بررسی سیستمی دیگر در خارج از مرزهای سیستم فعلی است و مشکل از جای دیگری نشات گرفته که اساسا هدف از تست ما بررسی عملکرد آن سیستم نبوده است. کلا در این موارد از mocking objects استفاده می‌شود:
- دسترسی به شیء مورد نظر کند است مانند دسترسی به دیتابیس یا محاسبات بسیار طولانی
- شیء مورد نظر از call back استفاده می‌کند
- شیء مورد آزمایش باید به منابع خارجی دسترسی پیدا کند که اکنون مهیا نیستند. برای مثال دسترسی به شبکه.
- شیءایی که می‌خواهیم آن‌را تست کنیم یا برای آن آزمایشات واحد تهیه نمائیم، هنوز کاملا توسعه نیافته و نیمه کاره است.
ب) تعریف رفتارهای مورد نظر یا Act
ج) بررسی رفتارهای تعریف شده یا Assert

مثال:
متد ساده زیر را در نظر بگیرید:

public class ImageManagement
{
public string GetImageForTimeOfDay()
{
int currentHour = DateTime.Now.Hour;
return currentHour > 6 && currentHour < 21 ? "sun.jpg" : "moon.jpg";
}

}
آزمایش این متد، وابسته است به زمان جاری سیستم.

using System;
using NUnit.Framework;

[TestFixture]
public class CMyTest
{
[Test]
public void DaytimeTest()
{
int currentHour = DateTime.Now.Hour;

if (currentHour > 6 && currentHour < 21)
{
const string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay();
Assert.AreEqual(expectedImagePath, path);
}
else
{
Assert.Ignore("تنها در طول روز قابل بررسی است");
}
}

[Test]
public void NighttimeTest()
{
int currentHour = DateTime.Now.Hour;

if (currentHour < 6 || currentHour > 21)
{
const string expectedImagePath = "moon.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay();
Assert.AreEqual(expectedImagePath, path);
}
else
{
Assert.Ignore("تنها در طول شب قابل بررسی است");
}
}

}
برای مثال اگر بخواهیم تصویر ماه را دریافت کنیم باید تا ساعت 21 صبر کرد. همچنین بررسی اینکه چرا یکی از متدهای آزمون واحد ما نیز با شکست مواجه شده است نیز نیازمند بررسی زمان جاری است و گاهی ممکن است با شکست مواجه شود و گاهی خیر. در این‌جا با استفاده از یک mock object ، این وضعیت غیرقابل پیش بینی را با منطقی از پیش طراحی شده جایگزین کرده و آزمون خود را بر اساس آن انجام خواهیم داد.
برای این‌کار باید DateTime.Now.Hour را تقلید نموده و اینترفیسی را بر اساس آن طراحی نمائیم. سپس Rhino Mocks کار پیاده سازی این اینترفیس را انجام خواهد داد:

using NUnit.Framework;
using Rhino.Mocks;

namespace testWinForms87
{
public interface IDateTime
{
int GetHour();
}

public class ImageManagement
{
public string GetImageForTimeOfDay(IDateTime time)
{
int currentHour = time.GetHour();

return currentHour > 6 && currentHour < 21 ? "sun.jpg" : "moon.jpg";
}
}

[TestFixture]
public class CMocking
{
[Test]
public void DaytimeTest()
{
MockRepository mocks = new MockRepository();
IDateTime timeController = mocks.CreateMock<IDateTime>();

using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(15);
}

using (mocks.Playback())
{
const string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);
}
}

[Test]
public void NighttimeTest()
{
MockRepository mocks = new MockRepository();
IDateTime timeController = mocks.CreateMock<IDateTime>();
using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(1);
}

using (mocks.Playback())
{
const string expectedImagePath = "moon.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);
}
}
}

}
همانطور که در ابتدای مطلب هم عنوان شد، mocking‌ از سه قسمت تشکیل می‌شود:

MockRepository mocks = new MockRepository();
ابتدا شیء mocks را از MockRepository کتابخانه Rhino Mocks ایجاد می‌کنیم تا بتوان از خواص و متدهای آن استفاده کرد.
سپس اینترفیسی باید به آن پاس شود تا انتظارات سیستم را بتوان در آن بر پا نمود:

IDateTime timeController = mocks.CreateMock<IDateTime>();
using (mocks.Record())
{
Expect.Call(timeController.GetHour()).Return(15);

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

همانطور که ملاحظه می‌کنید، روش Test Driven Development بر روی نحوه‌ی برنامه نویسی ما و ایجاد کلاس‌ها و اینترفیس‌های اولیه نیز تاثیر زیادی خواهد گذاشت. استفاده از اینترفیس‌ها یکی از اصول پایه‌ای برنامه نویسی شیءگرا است و در اینجا مقید به ایجاد آن‌ها خواهیم شد.

پس از آن‌که در قسمت mocks.Record ، انتظارات خود را ثبت کردیم، اکنون نوبت به وضعیت Playback می‌رسد:
using (mocks.Playback())
{
string expectedImagePath = "sun.jpg";
ImageManagement image = new ImageManagement();
string path = image.GetImageForTimeOfDay(timeController);
Assert.AreEqual(expectedImagePath, path);

}
در اینجا روش کار همانند ایجاد متدهای آزمون واحد متداولی است که تاکنون با آن‌ها آشنا شده‌ایم و تفاوتی ندارد.
با توجه به اینکه پس از تغییر طراحی متد GetImageForTimeOfDay ، این متد اکنون از شیء IDateTime به عنوان ورودی استفاده می‌کند، می‌توان پیاده سازی آن اینترفیس ‌را در آزمایشات واحد تقلید نمود و یا جایی که قرار است در برنامه استفاده شود، می‌تواند پیاده سازی واقعی خود را داشته باشد و دیگر آزمایشات ما وابسته به آن نخواهد بود:

public class DateTimeController : IDateTime
{
public int GetHour()
{
return DateTime.Now.Hour;
}
}

نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 11 - بررسی رابطه‌ی Self Referencing

یک نکته‌ی تکمیلی: تاثیر فراخوانی متد AsNoTracking بر روی کوئری‌های خود ارجاعی

همانطور که در مطلب «مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first» هم مشاهده کردید، خود EF، قابلیت تشکیل درخت نهایی خود ارجاع دهنده را دارد و به این ترتیب کوئری گرفتن از نتیجه‌ی آن، بسیار ساده می‌شود. اما ... اگر در این بین، از متد AsNoTracking برای بهینه سازی، کاهش میزان حافظه و حذف پروکسی‌های ردیابی تغییرات EF استفاده شود، دیگر این درخت خودکار، تشکیل نخواهد شد. برای پوشش این حالت می‌توان به صورت زیر عمل کرد:

الف) تشکیل یک کلاس پایه برای تعریف ساده‌تر و مشخص رابطه‌های خود ارجاعی

public abstract class BaseEntity
{
    public int Id { get; set; }
}

public abstract class BaseSelfReferencingEntity<TSelfEntity> : BaseEntity
    where TSelfEntity : BaseEntity
{
    public virtual TSelfEntity? Reply { set; get; }

    public int? ReplyId { get; set; }

    public virtual ICollection<TSelfEntity>? Children { get; set; }
}

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

ب) پر کردن درخت نهایی حاصل به صورت دستی:

چون دیگر EF این درخت را برای ما تشکیل نمی‌دهد، اکنون باید خودمان کار تشکیل آن‌را به صورت زیر انجام دهیم:

public static class SelfReferencingExtensions
{ 
    public static List<TEntity> ToSelfReferencingTree<TEntity>(this ICollection<TEntity>? originalList)
        where TEntity : BaseSelfReferencingEntity<TEntity>
    {
        var results = new List<TEntity>();

        if (originalList is null || originalList.Count == 0)
        {
            return results;
        }

        foreach (var rootItem in originalList.Where(x => !x.ReplyId.HasValue))
        {
            results.Add(rootItem);
            AppendChildren(originalList, rootItem);
        }

        return results;
    }

    private static void AppendChildren<TEntity>(ICollection<TEntity> originalList, TEntity parentItem)
        where TEntity : BaseSelfReferencingEntity<TEntity>
    {
        foreach (var kid in originalList.Where(x => x.ReplyId.HasValue && x.ReplyId.Value == parentItem.Id))
        {
            parentItem.Children ??= new List<TEntity>();
            parentItem.Children.Add(kid);
            AppendChildren(originalList, kid);
        }
    }
}

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

پس از این مقدمات، نحوه‌ی استفاده از آن به صورت زیر است:

var comments = await _comments.AsNoTracking()
            .Where(x => x.ParentId == postId)
            .OrderBy(x => x.Id)
            .Take(count)
            .ToListAsync();

var commentsTree = comments.ToSelfReferencingTree();

کوئری نویسی ابتدایی آن، کاملا استاندارد و بدون هیچگونه نکته‌ی خاصی است. ابتدا تمام نظرات یک مطلب (به صورت AsNoTracking) بازگشت داده می‌شوند و سپس متد ToSelfReferencingTree کار اتصالات نهایی درخت پاسخ‌ها را به صورت خودکار انجام می‌دهد.

نظرات مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
- خیر. یافتن یک کاربر بر اساس ID آن که کلید اصلی جدول را هم تشکیل می‌دهد و بر روی آن ایندکس وجود دارد، فوق العاده سریع است. یک چنین کوئری گرفتنی در ASP.NET Core Identity هم وجود دارد. البته ASP.NET Core Identity برای انجام اینکار، بیش از یک کوئری را صادر می‌کند.
- وجود ICookieValidatorService که سبب بروز این کوئری اضافه می‌شود، در جهت پیاده سازی logout آنی و بررسی به روز بودن محتوای کوکی دریافتی، ضروری هست. نیاز هست تا بتوان مقدار public string SerialNumber مطلب جاری را بین اطلاعات موجود در سرور و اطلاعات قبلی موجود در کوکی صادر شده‌ی در حین لاگین، مقایسه کرد. اگر این بررسی صورت نگیرد، تا زمانیکه طول عمر کوکی کاربر به پایان نرسیده، با موفقیت و بدون هیچ کوئری اضافه‌ای، اطلاعات موجود در آن، در سمت سرور اعتبارسنجی شده و استفاده می‌شود. از آن اطلاعات در جهت تامین خودکار user claims و roles کاربر جاری استفاده می‌شود (همان شیء this.User قابل دسترسی در اکشن متدها)؛ چون زیر ساخت ASP.NET Core در جهت بررسی اطلاعات کوکی رسیده، به همراه دریافت اطلاعات مجدد آن از بانک اطلاعاتی سمت سرور نیست. اگر کوکی را معتبر تشخیص دهد و هنوز طول عمر آن به پایان نرسیده باشد، از اطلاعات آن در جهت ساخت شیء this.User استفاده می‌کند. اینجا است که اهمیت وجودی ICookieValidatorService در جهت تامین یک لایه‌ی اضافی امنیتی بررسی محتوای کوکی دریافتی، مشخص می‌شود. برای مثال فرض کنید که کاربری را به دلیل خاصی غیرفعال یا ممنوع الورود کرده‌اید. اگر سرویس ICookieValidatorService را نداشته باشید، این کاربر تا زمانیکه طول عمر کوکی آن معتبر تشخیص داده شود، می‌تواند در سایت خراب کاری کند؛ چون اطلاعات کوکی او، شیء this.User معتبری را تولید می‌کند که وابسته‌ی به اطلاعات جدید بانک اطلاعاتی برنامه نیست. همچنین قرار دادن دستی بررسی دوباره‌ی اعتبار user claim حاصل از اطلاعات کوکی کاربر جاری در هر اکشن متد و مقایسه‌ی آن با اطلاعات جدید بانک اطلاعاتی، سبب شلوغ شدن و تکرار کدهای اضافه‌ای می‌شود که به صورت متمرکز و یکجا در ICookieValidatorService وجود دارد. نمونه‌ی دیگر کاربرد این سرویس، پیاده سازی خودکار مفهوم single sign out است؛ به این معنا که اگر کاربری logout کرد، بتوان با تغییر SerialNumber او در سمت سرور، سایر افرادی را که در همان لحظه از اکانت او (کوکی‌های معتبر او) در سیستم‌های دیگری استفاده می‌کنند، logout کرد.
- استفاده از کش، به همراه بحث invalidation دقیق آن هم در صورت بروز تغییرات در بانک اطلاعاتی هست. هستند کتابخانه‌هایی که این‌کارها را به صورت خودکار و بدون نیازی به تغییری در کدها برای شما انجام دهند.
نظرات مطالب
Implementing second level caching in EF code first
- همین پیاده سازی فوق رو با یک برنامه کنسول ویندوزی هم تست کردم، کار می‌کنه. می‌خواهید یک امتحانی بکنید. به نظر در پشت صحنه به صورت خودکار به memory cache سوئیچ میشه. فقط باید ارجاعی را به اسمبلی System.Web اضافه کنید.
- ضمن اینکه در برنامه‌های دسکتاپ این مساله اهمیت آنچنانی نداره؛ چون سطح دوم کش بیشتر جهت ارائه محتوایی یکسان و با دسترسی عمومی، به کاربران همزمان سایت کاربرد داره. عمده اطلاعات برنامه‌های دسکتاپ با سطح دسترسی خصوصی و مخصوص به یک کاربر است؛ در یک چنین مواردی نباید از سطح دوم کش استفاده کرد وگرنه به مشکلات امنیتی و فاش سازی اطلاعاتی که نباید عمومی شوند، منتهی خواهد شد (البته اگر مثلا از یک وب سرویس استفاده شده باشه؛ اگر همه چیز لوکال است، این مساله صادق نخواهد بود؛ اما باز هم نیازی به سطح دوم کش نیست. چون مهم‌ترین هدف آن کاهش بار بانک اطلاعاتی، در مراجعات مکرر کاربران است؛ که در حالت لوکال آنچنان معنی ندارد).
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت پنجم
مقدمات ساخت بلاگ مبتنی بر ember.js در قسمت قبل به پایان رسید. در این قسمت صرفا قصد داریم بجای استفاده از HTML 5 local storage از یک REST web service مانند یک ASP.NET Web API Controller و یا یک ASP.NET MVC Controller استفاده کنیم و اطلاعات نهایی را به سرور ارسال و یا از آن دریافت کنیم.


تنظیم Ember data برای کار با سرور

Ember data به صورت پیش فرض و در پشت صحنه با استفاده از Ajax برای کار با یک REST Web Service طراحی شده‌است و کلیه تبادلات آن نیز با فرمت JSON انجام می‌شود. بنابراین تمام کدهای سمت کاربر قسمت قبل نیز در این حالت کار خواهند کرد. تنها کاری که باید انجام شود، حذف تنظیمات ابتدایی آن برای کار با HTML 5 local storage است.
برای این منظور ابتدا فایل index.html را گشوده و سپس مدخل localstorage_adapter.js را از آن حذف کنید:
 <!--<script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script>-->
همچنین دیگر نیازی به store.js نیز نمی‌باشد:
 <!--<script src="Scripts/App/store.js" type="text/javascript"></script>-->

اکنون برنامه را اجرا کنید، چنین پیام خطایی را مشاهده خواهید کرد:

همانطور که عنوان شد، ember data به صورت پیش فرض با سرور کار می‌کند و در اینجا به صورت خودکار، یک درخواست Get را به آدرس http://localhost:25918/posts جهت دریافت آخرین مطالب ثبت شده، ارسال کرده‌است و چون هنوز وب سرویسی در برنامه تعریف نشده، با خطای 404 و یا یافت نشد، مواجه شده‌است.
این درخواست نیز بر اساس تعاریف موجود در فایل Scripts\Routes\posts.js، به سرور ارسال شده‌است:
Blogger.PostsRoute = Ember.Route.extend({
    model: function () {
        return this.store.find('post');
    }
});
Ember data شبیه به یک ORM عمل می‌کند. تنظیمات ابتدایی آن‌را تغییر دهید، بدون نیازی به تغییر در کدهای اصلی برنامه، می‌تواند با یک منبع داده جدید کار کند.


تغییر تنظیمات پیش فرض آغازین Ember data

آدرس درخواستی http://localhost:25918/posts به این معنا است که کلیه درخواست‌ها، به همان آدرس و پورت ریشه‌ی اصلی سایت ارسال می‌شوند. اما اگر یک ASP.NET Web API Controller را تعریف کنیم، نیاز است این درخواست‌ها، برای مثال به آدرس api/posts ارسال شوند؛ بجای /posts.
برای این منظور پوشه‌ی جدید Scripts\Adapters را ایجاد کرده و فایل web_api_adapter.js را با این محتوا به آن اضافه کنید:
 DS.RESTAdapter.reopen({
      namespace: 'api'
});
سپس تعریف مدخل آن‌را نیز به فایل index.html اضافه نمائید:
 <script src="Scripts/Adapters/web_api_adapter.js" type="text/javascript"></script>
تعریف فضای نام در اینجا سبب خواهد شد تا درخواست‌های جدید به آدرس api/posts ارسال شوند.


تغییر تنظیمات پیش فرض ASP.NET Web API

در سمت سرور، بنابر اصول نامگذاری خواص، نام‌ها با حروف بزرگ شروع می‌شوند:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}
اما در سمت کاربر و کدهای اسکریپتی، عکس آن صادق است. به همین جهت نیاز است که CamelCasePropertyNamesContractResolver را در JSON.NET تنظیم کرد تا به صورت خودکار اطلاعات ارسالی به کلاینت‌ها را به صورت camel case تولید کند:
using System;
using System.Web.Http;
using System.Web.Routing;
using Newtonsoft.Json.Serialization;
 
namespace EmberJS03
{
    public class Global : System.Web.HttpApplication
    {
 
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
 
            var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }
    }
}


نحوه‌ی صحیح بازگشت اطلاعات از یک ASP.NET Web API جهت استفاده در Ember data

با تنظیمات فوق، اگر کنترلر جدیدی را به صورت ذیل جهت بازگشت لیست مطالب تهیه کنیم:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public IEnumerable<Post> Get()
        {
            return DataSource.PostsList;
        } 
    }
}
با یک چنین خطایی در سمت کاربر مواجه خواهیم شد:
  WARNING: Encountered "0" in payload, but no model was found for model name "0" (resolved model name using DS.RESTSerializer.typeForRoot("0"))
این خطا از آنجا ناشی می‌شود که Ember data، اطلاعات دریافتی از سرور را بر اساس قرارداد JSON API دریافت می‌کند. برای حل این مشکل راه‌حل‌های زیادی مطرح شده‌اند که تعدادی از آن‌ها را در لینک‌های زیر می‌توانید مطالعه کنید:
http://jsonapi.codeplex.com
https://github.com/xqiu/MVCSPAWithEmberjs
https://github.com/rmichela/EmberDataAdapter
https://github.com/MilkyWayJoe/Ember-WebAPI-Adapter
http://blog.yodersolutions.com/using-ember-data-with-asp-net-web-api
http://emadibrahim.com/2014/04/09/emberjs-and-asp-net-web-api-and-json-serialization

و خلاصه‌ی آن‌ها به این صورت است:
خروجی JSON تولیدی توسط ASP.NET Web API چنین شکلی را دارد:
[
  {
    Id: 1,
    Title: 'First Post'
  }, {
    Id: 2,
    Title: 'Second Post'
  }
]
اما Ember data نیاز به یک چنین خروجی دارد:
{
  posts: [{
    id: 1,
    title: 'First Post'
  }, {
    id: 2,
    title: 'Second Post'
  }]
}
به عبارتی آرایه‌ی مطالب را از ریشه‌ی posts باید دریافت کند (مطابق فرمت JSON API). برای انجام اینکار یا از لینک‌های معرفی شده استفاده کنید و یا راه حل ساده‌ی ذیل هم پاسخگو است:
using System.Web.Http;
using EmberJS03.Models;
 
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public object Get()
        {
            return new { posts = DataSource.PostsList };
        }
    }
}
در اینجا ریشه‌ی posts را توسط یک anonymous object ایجاد کرده‌ایم.
اکنون اگر برنامه را اجرا کنید، در صفحه‌ی اول آن، لیست عناوین مطالب را مشاهده خواهید کرد.


تاثیر قرارداد JSON API در حین ارسال اطلاعات به سرور توسط Ember data

در تکمیل کنترلرهای Web API مورد نیاز (کنترلرهای مطالب و نظرات)، نیاز به متدهای Post، Update و Delete هم خواهد بود. دقیقا فرامین ارسالی توسط Ember data توسط همین HTTP Verbs به سمت سرور ارسال می‌شوند. در این حالت اگر متد Post کنترلر نظرات را به این شکل طراحی کنیم:
 public HttpResponseMessage Post(Comment comment)
کار نخواهد کرد؛ چون مطابق فرمت JSON API ارسالی توسط Ember data، یک چنین شیء JSON ایی را دریافت خواهیم کرد:
{"comment":{"text":"data...","post":"3"}}
بنابراین Ember data چه در حین دریافت اطلاعات از سرور و چه در زمان ارسال اطلاعات به آن، اشیاء جاوا اسکریپتی را در یک ریشه‌ی هم نام آن شیء قرار می‌دهد.
برای پردازش آن، یا باید از راه حل‌های ثالث مطرح شده در ابتدای بحث استفاده کنید و یا می‌توان مطابق کدهای ذیل، کل اطلاعات JSON ارسالی را توسط کتابخانه‌ی JSON.NET نیز پردازش کرد:
namespace EmberJS03.Controllers
{
    public class CommentsController : ApiController
    {
        public HttpResponseMessage Post(HttpRequestMessage requestMessage)
        {
            var jsonContent = requestMessage.Content.ReadAsStringAsync().Result;
            // {"comment":{"text":"data...","post":"3"}}
            var jObj = JObject.Parse(jsonContent);
            var comment = jObj.SelectToken("comment", false).ToObject<Comment>();


            var id = 1;
            var lastItem = DataSource.CommentsList.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            comment.Id = id;
            DataSource.CommentsList.Add(comment);

            // ارسال آی دی با فرمت خاص مهم است
            return Request.CreateResponse(HttpStatusCode.Created, new { comment = comment });
        }
    }
}
در اینجا توسط requestMessage به محتوای ارسال شده‌ی به سرور که همان شیء JSON ارسالی است، دسترسی خواهیم داشت. سپس متد JObject.Parse، آن‌را به صورت عمومی تبدیل به یک شیء JSON می‌کند و نهایتا با استفاده از متد SelectToken آن می‌توان ریشه‌ی comment و یا در کنترلر مطالب، ریشه‌ی post را انتخاب و سپس تبدیل به شیء Comment و یا Post کرد.
همچنین فرمت return نهایی هم مهم است. در این حالت خروجی ارسالی به سمت کاربر، باید مجددا با فرمت JSON API باشد؛ یعنی باید comment اصلاح شده را به همراه ریشه‌ی comment ارسال کرد. در اینجا نیز anonymous object تهیه شده، چنین کاری را انجام می‌دهد.


Lazy loading در Ember data

تا اینجا اگر برنامه را اجرا کنید، لیست مطالب صفحه‌ی اول را مشاهده خواهید کرد، اما لیست نظرات آن‌ها را خیر؛ از این جهت که ضرورتی نداشت تا در بار اول ارسال لیست مطالب به سمت کاربر، تمام نظرات متناظر با آن‌ها را هم ارسال کرد. بهتر است زمانیکه کاربر یک مطلب خاص را مشاهده می‌کند، نظرات خاص آن‌را به سمت کاربر ارسال کنیم.
در تعاریف سمت کاربر Ember data، پارامتر دوم رابطه‌ی hasMany که با async:true مشخص شده‌است، دقیقا معنای lazy loading را دارد.
Blogger.Post = DS.Model.extend({
   title: DS.attr(),
   body: DS.attr(),
   comments: DS.hasMany('comment', { async: true } /* lazy loading */)
});
در سمت سرور، دو راه برای فعال سازی این lazy loading تعریف شده در سمت کاربر وجود دارد:
الف) Idهای نظرات هر مطلب را به صورت یک آرایه، در بار اول ارسال لیست نظرات به سمت کاربر، تهیه و ارسال کنیم:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
 
        // lazy loading via an array of IDs
        public int[] Comments { set; get; } 
    }
}
در اینجا خاصیت Comments، تنها کافی است لیستی از Idهای نظرات مرتبط با مطلب جاری باشد. در این حالت در سمت کاربر اگر مطلب خاصی جهت مشاهده‌ی جزئیات آن انتخاب شود، به ازای هر Id ذکر شده، یکبار دستور Get صادر خواهد شد.
ب) این روش به علت تعداد رفت و برگشت بیش از حد به سرور، کارآیی آنچنانی ندارد. بهتر است جهت مشاهده‌ی جزئیات یک مطلب، تنها یکبار درخواست Get کلیه نظرات آن صادر شود.
برای اینکار باید مدل برنامه را به شکل زیر تغییر دهیم:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
 
        // load related models via URLs instead of an array of IDs
        // ref. https://github.com/emberjs/data/pull/1371
        public object Links { set; get; }
 
        public Post()
        {
            Links = new { comments = "comments" }; // api/posts/id/comments
        }
    }
}
در اینجا یک خاصیت جدید به نام Links ارائه شده‌است. نام Links در Ember data استاندارد است و از آن برای دریافت کلیه اطلاعات لینک شده‌ی به یک مطلب استفاده می‌شود. با تعریف این خاصیت به نحوی که ملاحظه می‌کنید، اینبار Ember data تنها یکبار درخواست ویژه‌ای را با فرمت api/posts/id/comments، به سمت سرور ارسال می‌کند. برای مدیریت آن، قالب مسیریابی پیش فرض {api/{controller}/{id را می‌توان به صورت {api/{controller}/{id}/{name اصلاح کرد:
namespace EmberJS03
{
    public class Global : System.Web.HttpApplication
    {
 
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}/{name}",
               defaults: new { id = RouteParameter.Optional, name = RouteParameter.Optional }
               );
 
            var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }
    }
}
اکنون دیگر درخواست جدید api/posts/3/comments با پیام 404 یا یافت نشد مواجه نمی‌شود.
در این حالت در طی یک درخواست می‌توان کلیه نظرات را به سمت کاربر ارسال کرد. در اینجا نیز ذکر ریشه‌ی comments همانند ریشه posts، الزامی است:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        //GET api/posts/id
        public object Get(int id)
        {
            return
                new
                {
                    posts = DataSource.PostsList.FirstOrDefault(post => post.Id == id),
                    comments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList()
                };
        }
    }
}


پردازش‌های async و متد transitionToRoute در Ember.js

اگر متد حذف مطالب را نیز به کنترلر Posts اضافه کنیم:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = DataSource.PostsList.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            DataSource.PostsList.Remove(item);

            //حذف کامنت‌های مرتبط
            var relatedComments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList();
            relatedComments.ForEach(comment => DataSource.CommentsList.Remove(comment));

            return Request.CreateResponse(HttpStatusCode.OK, new { post = item });
        }
    }
}
قسمت سمت سرور کار تکمیل شده‌است. اما در سمت کاربر، چنین خطایی را دریافت خواهیم کرد:
 Attempted to handle event `pushedData` on  while in state root.deleted.inFlight.
منظور از حالت inFlight در اینجا این است که هنوز کار حذف سمت سرور تمام نشده‌است که متد transitionToRoute را صادر کرده‌اید. برای اصلاح آن، فایل Scripts\Controllers\post.js را باز کرده و پس از متد destroyRecord، متد then را قرار دهید:
Blogger.PostController = Ember.ObjectController.extend({
    isEditing: false,
    actions: {
        edit: function () {
            this.set('isEditing', true);
        },
        save: function () {
            var post = this.get('model');
            post.save();
 
            this.set('isEditing', false);
        },
        delete: function () {
            if (confirm('Do you want to delete this post?')) {
                var thisController = this;
                var post = this.get('model');
                post.destroyRecord().then(function () {
                    thisController.transitionToRoute('posts');
                });
            }
        }
    }
});
به این ترتیب پس از پایان عملیات حذف سمت سرور، قسمت then اجرا خواهد شد. همچنین باید دقت داشت که this اشاره کننده به کنترلر جاری را باید پیش از فراخوانی then ذخیره و استفاده کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_05.zip
نظرات مطالب
MVC vs 3-Tier Pattern
درک من از معماری سه لایه یا N-Teir اینگونه است که
اصولا باید تقسیم بندی لایه‌ها به این صورت باشه که:
1-لایه زیرساخت که متشکل از DataLayer,ServiceLayer,domainclasses هست
2-لایه وب سرویس که می‌تواند یک سرویس دهنده باشد مثل WebApi,Wcf
3-لایه ui که هر نوع appی می‌تواند باشد .مانند Asp.Net Webforms,Asp.Net Mvc,Andriod ,Ios,Winphone و یا حتی php
در واقع لایه وب سرویس هست که لایه ui رو تغذیه میکند.
لایه ui با ارسال درخواست به لایه وب سرویس داده  مورد نظر خود را دریافت میکند و مهم نیست که این لایه از چه نوع تکنولوژی استفاده میکند.
اینگونه است که میتوان وابستگی لایه‌ها را تفکیک کرد و به یک معماری مستقل رسید.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت اول - نیاز به تامین کننده‌ی هویت مرکزی
عصر Thick Clients

امن سازی برنامه‌های وب همواره چالش برانگیز بوده‌است؛ خصوصا این روزها که نیاز است برنامه‌ها، خارج از دیوارهای یک شرکت نیز در دسترس باشند و توسط انواع و اقسام وسایل ارتباطی مورد استفاده قرار گیرند. در سال‌های قبل، عموما برنامه‌های thick clients مانند WPF و WinForms برای شرکت‌ها توسعه داده می‌شدند و یا برنامه‌های وب مانند ASP.NET Web Forms که مبتنی بر سرویس‌ها نبودند. در برنامه‌های ویندوزی، پس از لاگین شخص به شبکه و دومین داخلی شرکت، عموما از روش Windows Authentication برای مشخص سازی سطوح دسترسی کاربران استفاده می‌شود. در برنامه‌های Web Forms نیز بیشتر روش Forms Authentication برای اعتبارسنجی کاربران مرسوم است. امن سازی این نوع برنامه‌ها ساده‌است. عموما بر روی یک دومین ارائه می‌شوند و کوکی‌های اعتبارسنجی کاربران برای ارائه‌ی مباحثی مانند single sign-on (داشتن تنها یک صفحه‌ی لاگین برای دسترسی به تمام برنامه‌های شرکت)، میسر است.


عصر شروع به‌کارگیری سرویس‌های وب

در سال‌های اخیر این شیوه‌ی کاری تغییر کرده و بیشتر بر اساس بکارگیری برنامه‌های مبتنی بر سرویس‌ها شده‌است. برای مثال برای این مورد استاندارد WS-Security مربوط به WCF ارائه شده‌است که باز هم مرتبط است به برنامه‌های یک دومین و یا یک Application pool. اگر تعداد دومین‌ها بیشتر شوند و نیاز به ارتباط امن بین آن‌ها باشد، استاندارد SAML 2.0 مورد استفاده قرار می‌گرفت که هدف از آن، انتقال امن اعتبارسنجی و سطوح دسترسی کاربران بین دومین‌های مختلف است. همانطور که ملاحظه می‌کنید تمام این برنامه‌ها و استانداردها، داخل دیوارهای یک شرکت و یک دومین زندگی می‌کنند.


عصر شروع به‌کارگیری Restful-API's

پس از آن باز هم شیوه‌ی طراحی برنامه‌های تغییر کرد و شروع به ایجاد Restful-API's و HTTP API's کردیم. این‌ها دیگر الزاما داخل یک دومین ارائه نمی‌شوند و گاهی از اوقات حتی تحت کنترل ما هم نیستند. همچنین برنامه‌های ارائه شده نیز دیگر thick clients نیستند و ممکن است برنامه‌های سمت کلاینت Angular و یا حتی موبایل باشند که مستقیما با سرویس‌های API برنامه‌ها کار می‌کنند. حتی ممکن است یک API را طراحی کنیم که با یک API دیگر کار می‌کند.


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


عصر شروع به‌کارگیری Security Tokens

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


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


حرکت به سمت «تامین کننده‌ی هویت مرکزی»


در گذشته، هر تک برنامه‌ای دارای صفحه‌ی لاگین و امکانات مدیریت کاربران آن، تغییر کلمه‌ی عبور، تنظیم مجدد آن و این‌گونه عملیات بود. این‌روزها دیگر چنین کاری مرسوم نیست. این وظیفه‌ی برنامه‌ی شما نیست که بررسی کند کاربر وارد شده‌ی به سیستم کیست و آیا ادعای او صحیح است یا خیر؟ این نوع عملیات وظیفه‌ی یک Identity provider و یا به اختصار IDP است. کار IDP اعتبارسنجی کاربر و در صورت نیاز، ارائه‌ی اثبات هویت کاربر، به برنامه‌ی درخواست کننده‌است.
در یک IDP عملیاتی مانند ثبت کاربران و مدیریت آن‌ها انجام می‌شود. اینجا است که مفاهیمی مانند قفل کردن اکانت و یا تغییر کلمه‌ی عبور و امثال آن انجام می‌شود و نه اینکه به ازای هر برنامه‌ی تهیه شده‌ی برای یک شرکت، آن برنامه راسا اقدام به انجام چنین عملیاتی کند. به این ترتیب می‌توان به امکان استفاده‌ی مجدد از اطلاعات هویت کاربران و سطوح دسترسی آن‌ها در بین تمام برنامه‌های موجود رسید.
همچنین با داشتن یک برنامه‌ی IDP خوب پیاده سازی شده، از توزیع باگ‌های امنیتی مختلف در بین برنامه‌های گوناگون تهیه شده که هر کدام سیستم امنیتی خاص خودشان را دارند، جلوگیری خواهد شد. برای مثال فرض کنید می‌خواهید الگوریتم هش کردن پسوردهای سیستم را که امروز نا امن اعلام شده‌است، تغییر دهید. با داشتن یک IDP، دیگر نیازی نیست تا تمام برنامه‌های خود را برای رفع یک چنین باگ‌هایی، تک تک تغییر دهید.


به علاوه این روزها روش استفاده‌ی از نام کاربری و کلمه‌ی عبور، تنها راه ورود به یک سیستم نیست و استفاده از کلیدهای دیجیتال و یا روش‌های ویژه‌ی ابزارهای موبایل نیز به این لیست اضافه شده‌اند.


حرکت به سمت استاندارد OAuth 2

OAuth 2.0 پروتکلی است استاندارد، برای Authorization امن کاربران، توسط برنامه‌های وب، موبایل و دسکتاپ. به این ترتیب می‌توان امکان دسترسی یک برنامه را به یک API، به نحوی استاندارد و امن میسر ساخت. OAuth 2.0 یک توکن دسترسی (Access token) را تولید می‌کند و در اختیار کلاینت قرار می‌دهد. سپس آن کلاینت با ارسال این توکن به API مدنظر، امکان دسترسی به امکانات مختلف آن‌را خواهد یافت. به علاوه چون ماهیت برنامه‌های کلاینت وب و غیر وب متفاوت است، این استاندارد نحوه‌ی دریافت چنین توکنی را برای برنامه‌های مختلف نیز تعریف می‌کند. به این ترتیب موارد مشترکی مانند تولید و نحوه‌ی انتقال توکن‌ها به کلاینت‌های مختلف توسط این پروتکل استاندارد بیان می‌شود. در این حالت راه‌حل‌های خانگی ما تبدیل به راه‌حل‌هایی می‌شوند که استاندارد OAuth 2.0 را پیاده سازی کرده باشند. بنابراین IDP ما باید بر مبنای این استاندارد تهیه شده باشد. برای مثال IdentityServer که در این سری بررسی خواهد شد و یا Azure Active Directory، نمونه‌ای از IDPهایی هستند که این استاندارد را کاملا پیاده سازی کرده‌اند.
البته باید دقت داشت که این توکن‌های دسترسی، تنها سطوح دسترسی به منابع API را مشخص می‌کنند و یا به عبارتی عملیات Authorization توسط آن‌ها میسر می‌شود. عملیات ورود به سیستم اصطلاحا Authentication نام دارد و توسط استاندارد دیگری به نام OpenID Connect مدیریت می‌شود.


حرکت به سمت استاندارد OpenID Connect


OpenID Connect یک لایه‌ی امنیتی بر فراز پروتکل OAuth 2.0 است که به اختصار به آن OIDC نیز گفته می‌شود. توسط آن یک کلاینت می‌تواند یک Identity token را علاوه بر Access token درخواست کند. از این Identity token برای ورود به برنامه‌ی کلاینت استفاده می‌شود (Authentication) و پس از آن، برنامه‌ی کلاینت بر اساس سطوح دسترسی تعریف شده‌ی در Access token، امکان دسترسی به امکانات مختلف یک API را خواهد یافت (Authorization). همچنین OpenID Connect امکان دسترسی به اطلاعات بیشتری از یک کاربر را نیز میسر می‌کند.
بنابراین OpenID Connect پروتکلی است که در عمل استفاده می‌شود و توسعه دهنده و جایگزین کننده‌ی پروتکل OAuth 2.0 می‌باشد. هرچند ممکن است در بسیاری از منابع صرفا به OAuth 2.0 بپردازند، اما در پشت صحنه با همان OpenID Connect کار می‌کنند.
مزیت دیگر کار با OpenID Connect، عدم الزام به استفاده‌ی از API، در برنامه‌ای خاص و یا قدیمی است. اگر برنامه‌ی وب شما با هیچ نوع API ایی کار نمی‌کند، باز هم می‌توانید از امکانات OpenID Connect بهره‌مند شوید.
مطالب
Roslyn #7
معرفی Workspace API

Workspace، در حقیقت نمایش اجزای یک Solution در ویژوال استودیو است و یک Solution متشکل است از تعدادی پروژه به همراه وابستگی‌های بین آن‌ها. هدف از وجود Workspace API در Roslyn، دسترسی به اطلاعات لازم جهت انجام امور Refactoring در سطح یک Solution است. برای مثال اگر قرار است نام خاصیتی تغییر کند و این خاصیت در چندین پروژه‌ی دیگر در حال استفاده است، این نام باید در سراسر Solution جاری یافت شده و تغییر یابد. همچنین برفراز Workspace API تعدادی سرویس زبان مانند فرمت کننده‌های کدها، تغییرنام دهنده‌های سیمبل‌ها و توصیه کننده‌ها نیز تهیه شده‌اند.
همچنین این سرویس‌ها و API تهیه شده، منحصر به ویژوال استودیو نیستند و VS 2015 تنها از آن‌ها استفاده می‌کند. برای مثال نگارش‌های جدیدتر mono-develop لینوکسی نیز شروع به استفاده‌ی از Roslyn کرده‌اند.


نمایش اجزای یک Solution

 در ادامه مثالی را مشاهده می‌کنید که توسط آن نام Solution و سپس تمام پروژه‌های موجود در آن‌ها به همراه نام فایل‌های مرتبط و همچنین ارجاعات آن‌ها در صفحه نمایش داده می‌شوند:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Print the root of the solution.
Console.WriteLine(Path.GetFileName(sln.FilePath));
 
 
// Get dependency graph to perform a sort.
var g = sln.GetProjectDependencyGraph();
var ps = g.GetTopologicallySortedProjects();
 
 
// Print all projects, their documents, and references.
foreach (var p in ps)
{
    var proj = sln.GetProject(p);
 
    Console.WriteLine("> " + proj.Name);
 
    Console.WriteLine("  > References");
    foreach (var r in proj.ProjectReferences)
    {
        Console.WriteLine("    - " + sln.GetProject(r.ProjectId).Name);
    }
 
    foreach (var d in proj.Documents)
    {
        Console.WriteLine("  - " + d.Name);
    }
}
در ابتدا نیاز است یک وهله از MSBuildWorkspace را ایجاد کرد. اکنون با استفاده از این Workspace می‌توان solution خاصی را گشود و آنالیز کرد. قسمتی از خروجی آن چنین شکلی را دارد:
 Roslyn.sln
> Roslyn01
  > References
  - Program.cs
  - AssemblyInfo.cs
  - .NETFramework,Version=v4.6.AssemblyAttributes.cs


ایجاد یک Syntax highlighter با استفاده از Classification service

هدف از Classification service، رندر کردن فایل‌ها در ادیتور جاری است. برای این منظور نیاز است بتوان واژه‌های کلیدی، کامنت‌ها، نام‌های نوع‌ها و امثال آن‌ها را به صورت کلاسه شده در اختیار داشت و سپس برای مثال هرکدام را با رنگی مجزا نمایش داد و رندر کرد.
در ادامه مثالی از آن‌را ملاحظه می‌کنید:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;

// Get the Tests\Bar.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var test = proj.Documents.Single(d => d.Name == "Bar.cs");
 
var tree = test.GetSyntaxTreeAsync().Result;
var root = tree.GetRootAsync().Result;
 
// Get all the spans in the document that are classified as language elements.
var spans = Classifier.GetClassifiedSpansAsync(test, root.FullSpan).Result.ToDictionary(c => c.TextSpan.Start, c => c);
 
// Print the source text with appropriate colorization.
var txt = tree.GetText().ToString();
 
var i = 0;
foreach (var c in txt)
{
    var span = default(ClassifiedSpan);
    if (spans.TryGetValue(i, out span))
    {
        var color = ConsoleColor.Gray;
 
        switch (span.ClassificationType)
        {
            case ClassificationTypeNames.Keyword:
                color = ConsoleColor.Cyan;
                break;
            case ClassificationTypeNames.StringLiteral:
            case ClassificationTypeNames.VerbatimStringLiteral:
                color = ConsoleColor.Red;
                break;
            case ClassificationTypeNames.Comment:
                color = ConsoleColor.Green;
                break;
            case ClassificationTypeNames.ClassName:
            case ClassificationTypeNames.InterfaceName:
            case ClassificationTypeNames.StructName:
            case ClassificationTypeNames.EnumName:
            case ClassificationTypeNames.TypeParameterName:
            case ClassificationTypeNames.DelegateName:
                color = ConsoleColor.Yellow;
                break;
            case ClassificationTypeNames.Identifier:
                color = ConsoleColor.DarkGray;
                break;
        }
 
        Console.ForegroundColor = color;
    }
 
    Console.Write(c);
 
    i++;
}
با این خروجی:


توضیحات:
در اینجا نیز کار با ایجاد یک Workspace و سپس گشودن Solution ایی مشخص در آن آغاز می‌شود. سپس در آن به دنبال پروژه‌ای به نام Roslyn04.Tests می‌گردیم. این پروژه حاوی تعدادی کلاس، جهت بررسی و آزمایش هستند. برای مثال در اینجا فایل Bar.cs آن قرار است آنالیز شود. پس از یافتن آن، ابتدا syntax tree آن دریافت می‌گردد و سپس به سرویس Classifier.GetClassifiedSpansAsync ارسال خواهد شد. خروجی آن شامل لیستی از Classified Spans است؛ مانند کلمات کلیدی، رشته‌ها، کامنت‌ها و غیره. در ادامه این لیست تبدیل به یک دیکشنری می‌شود که کلید آن محل آغاز این span و مقدار آن، مقدار span است. سپس متن syntax tree دریافت شده و حرف به حرف آن در طی یک حلقه بررسی می‌شود. در این حلقه، مقدار i به محل حروف جاری مورد آنالیز اشاره می‌کند. اگر این محل در دیکشنری Classified Spans وجود داشت، یعنی یک span جدید شروع شده‌است و بر این اساس، نوع آن span را می‌توان استخراج کرد و سپس بر اساس این نوع، رنگ متفاوتی را در صفحه نمایش داد.


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

این سرویس کار فرمت خودکار کدهای بهم ریخته را انجام می‌دهد؛ مانند تنظیم فاصله‌های خالی و یا ایجاد indentation و امثال آن. در حقیقت Ctlr K+D در ویژوال استودیو، دقیقا از همین سرویس زبان استفاده می‌کند.
کار کردن با این سرویس از طریق برنامه نویسی به نحو ذیل است:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get the Tests\Qux.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var qux = proj.Documents.Single(d => d.Name == "Qux.cs");
 
Console.WriteLine("Before:");
Console.WriteLine();
Console.WriteLine(qux.GetSyntaxTreeAsync().Result.GetText());
 
Console.WriteLine();
Console.WriteLine();
 
 
// Apply formatting and print the result.
var res = Formatter.FormatAsync(qux).Result;
 
Console.WriteLine("After:");
Console.WriteLine();
Console.WriteLine(res.GetSyntaxTreeAsync().Result.GetText());
Console.WriteLine();
با این خروجی:
Before:

using System;

namespace Roslyn04.Tests
{
    class Qux {
        public void Baz()
        { Console.WriteLine(42);
            return;  }
    }
}


After:

using System;

namespace Roslyn04.Tests
{
    class Qux
    {
        public void Baz()
        {
            Console.WriteLine(42);
            return;
        }
    }
}
همانطور که ملاحظه می‌کنید، فایل Qux.cs که فرمت مناسبی ندارد. بنابراین باز شده و syntax tree آن به سرویس Formatter.FormatAsync جهت فرمت شدن ارسال می‌شود.


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

یکی دیگر از قابلیت‌هایی که در ویژوال استودیو وجود دارد، امکان یافتن سیمبل‌ها است. برای مثال این نوع یا کلاس خاص، در کجاها استفاده شده‌است و به آن ارجاعاتی وجود دارد. مواردی مانند Find all references، Go to definition و نمایش Call hierarchy از این سرویس استفاده می‌کنند.
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get the Tests project.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
 
// Locate the symbol for the Bar.Foo method and the Bar.Qux property.
var comp = proj.GetCompilationAsync().Result;
 
var barType = comp.GetTypeByMetadataName("Roslyn04.Tests.Bar");
 
var fooMethod = barType.GetMembers().Single(m => m.Name == "Foo");
var quxProp = barType.GetMembers().Single(m => m.Name == "Qux");
 
 
// Find callers across the solution.
Console.WriteLine("Find callers of Foo");
Console.WriteLine();
 
var callers = SymbolFinder.FindCallersAsync(fooMethod, sln).Result;
foreach (var caller in callers)
{
    Console.WriteLine(caller.CallingSymbol);
    foreach (var location in caller.Locations)
    {
        Console.WriteLine("    " + location);
    }
}
 
Console.WriteLine();
Console.WriteLine();
 
// Find all references across the solution.
Console.WriteLine("Find all references to Qux");
Console.WriteLine();
 
var references = SymbolFinder.FindReferencesAsync(quxProp, sln).Result;
foreach (var reference in references)
{
    Console.WriteLine(reference.Definition);
    foreach (var location in reference.Locations)
    {
        Console.WriteLine("    " + location.Location);
    }
}
در این مثال، پروژه‌ی Roslyn04.Tests که حاوی کلاس‌های Foo و Qux است، جهت آنالیز باز شده‌است. در اینجا برای رسیدن به Symbols نیاز است ابتدا به Compilation API دسترسی یافت و سپس متادیتاها را بر اساس آن استخراج کرد. سپس متدهای Foo و خاصیت Qux آن یافت شده‌اند.
اکنون با استفاده از سرویس SymbolFinder.FindCallersAsync تمام فراخوان‌های متد Foo را در سراسر Solution جاری می‌یابیم.
سپس با استفاده از سرویس SymbolFinder.FindReferencesAsync تمام ارجاعات به خاصیت Qux را در Solution جاری نمایش می‌دهیم.


سرویس توصیه کننده

Intellisense در ویژوال استودیو از سرویس توصیه کننده‌ی Roslyn استفاده می‌کند.
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;

// Get the Tests\Foo.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var foo = proj.Documents.Single(d => d.Name == "Foo.cs");
 
 
// Find the 'dot' token in the first Console.WriteLine member access expression.
var tree = foo.GetSyntaxTreeAsync().Result;
var model = proj.GetCompilationAsync().Result.GetSemanticModel(tree);
var consoleDot = tree.GetRoot().DescendantNodes().OfType<MemberAccessExpressionSyntax>().First().OperatorToken;
 
 
// Get recommendations at the indicated cursor position.
//
//   Console.WriteLine
//           ^
var res = Recommender.GetRecommendedSymbolsAtPosition(

                    model, consoleDot.GetLocation().SourceSpan.Start + 1, ws).ToList();
 
foreach (var rec in res)
{
    Console.WriteLine(rec);
}
در این مثال سعی شده‌است لیست توصیه‌های ارائه شده در حین تایپ دات، توسط سرویس Recommender.GetRecommendedSymbolsAtPosition دریافت و نمایش داده شوند. در ابتدای کار، کلاس Foo گشوده شده و سپس Syntax tree و Semantic model آن استخراج می‌شود. این model پارامتر اول متد سرویس توصیه کننده‌است. سپس نیاز است محل مکانی را به آن معرفی کنیم تا کار توصیه کردن را بر اساس آن شروع کند. برای نمونه در اینجا OperatorToken در حقیقت همان دات مربوط به Console.WriteLine است. پس از یافتن این توکن، امکان دسترسی به مکان آن وجود دارد.
تعدادی از خروجی‌های مثال فوق به صورت زیر هستند:
 System.Console.Beep()
System.Console.Beep(int, int)
System.Console.Clear()


سرویس تغییر نام دادن

هدف از سرویس Renamer.RenameSymbolAsync، تغییر نام یک identifier در کل Solution است. نمونه‌ای از نحوه‌ی کاربرد آن‌را در مثال ذیل مشاهده می‌کنید:
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get Tests\Bar.cs before making changes.
var oldProj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var oldDoc = oldProj.Documents.Single(d => d.Name == "Bar.cs");
 
Console.WriteLine("Before:");
Console.WriteLine();
 
var oldTxt = oldDoc.GetTextAsync().Result;
Console.WriteLine(oldTxt);
 
Console.WriteLine();
Console.WriteLine();
 
 
// Get the symbol for the Bar.Foo method.
var comp = oldProj.GetCompilationAsync().Result;
 
var barType = comp.GetTypeByMetadataName("Roslyn04.Tests.Bar");
var fooMethod = barType.GetMembers().Single(m => m.Name == "Foo");
 
 
// Perform the rename.
var newSln = Renamer.RenameSymbolAsync(sln, fooMethod, "Foo2", ws.Options).Result;
 
 
// Get Tests\Bar.cs after making changes.
var newProj = newSln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var newDoc = newProj.Documents.Single(d => d.Name == "Bar.cs");
 
Console.WriteLine("After:");
Console.WriteLine();
 
var newTxt = newDoc.GetTextAsync().Result;
Console.WriteLine(newTxt);
در این مثال، متد Foo کلاس Bar، قرار است به Foo2 تغییرنام یابد. به همین منظور ابتدا پروژه‌ی حاوی فایل Bar.cs باز شده و اطلاعات این کلاس استخراج می‌گردد. سپس اصل این کلاس تغییر نیافته نمایش داده می‌شود. در ادامه با استفاده از API کامپایل، به متادیتای متد Foo یا به عبارتی Symbol آن دسترسی پیدا می‌کنیم. سپس این Symbol به متد یا سرویس Renamer.RenameSymbolAsync ارسال می‌شود تا کار تغییر نام صورت گیرد. پس از اینکار مجددا متن کلاس تغییر یافته نمایش داده خواهد شد.


سرویس ساده کننده

هدف از سرویس ساده کننده، ساده‌کردن و کاهش کدهای ارائه شده، از دید Semantics است. برای مثال اگر فضای نامی در قسمت using ذکر شده‌است، دیگر نیازی نیست تا این فضای نام به ابتدای فراخوانی یک متد آن اضافه شود و می‌توان این قطعه از کد را ساده‌تر کرد و کاهش داد.
var ws = MSBuildWorkspace.Create();
var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result;


// Get the Tests\Baz.cs document.
var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests");
var baz = proj.Documents.Single(d => d.Name == "Baz.cs");
 
Console.WriteLine("Before:");
Console.WriteLine();
Console.WriteLine(baz.GetSyntaxTreeAsync().Result.GetText());
 
Console.WriteLine();
Console.WriteLine();
 
var oldRoot = baz.GetSyntaxRootAsync().Result;

 
var memberAccesses = oldRoot.DescendantNodes().OfType<CastExpressionSyntax>();
var newRoot = oldRoot.ReplaceNodes(memberAccesses, (_, m) => m.WithAdditionalAnnotations(Simplifier.Annotation));
 
var newDoc = baz.WithSyntaxRoot(newRoot);
 
 
// Invoke the simplifier and print the result.
var res = Simplifier.ReduceAsync(newDoc).Result;
 
Console.WriteLine("After:");
Console.WriteLine();
Console.WriteLine(res.GetSyntaxTreeAsync().Result.GetText());
Console.WriteLine();
در این مثال نحوه‌ی ساده سازی cast‌های اضافی را ملاحظه می‌کنید. برای مثال اگر نوع متغیری int است، دیگر نیازی نیست در سراسر کد در کنار این متغیر، cast به int را هم ذکر کرد و می‌توان این کد را ساده‌تر نمود.


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید:
Roslyn-Samples.zip
مطالب
آموزش Prism #3
در پست‌های قبلی با Prism و روش استفاده از آن آشنا شدیم (قسمت اول) و (قسمت دوم). در این پست با استفاده از Mef قصد ایجاد یک پروژه Silverlight رو به صورت ماژولار داریم. مثال پیاده سازی شده در پست قبلی را در این پست به صورت دیگر پیاده سازی خواهیم کرد.
تفاوت‌های پیاده سازی مثال پست قبلی با این پست:
  • در مثال قبل پروژه به صورت Desktop و با WPF پیاده سازی شده بود ولی در این مثال با Silverlight می‌باشد؛
  • در مثال قبل از UnityBootstrapper استفاده شده بود ولی در این مثال از MefBootstrapper؛
  • در مثال قبل هر View در یک ماژول قرار داشت ولی در این مثال هر دو View را در یک ماژول قرار دادم؛
  • در مثال قبل از Prism Libary 2.x استفاده شده بود ولی در این مثال از PrismLibrary 4.x؛
  • و...

نکته : برای فهم بهتر مفاهیم، آشنایی اولیه با MEF و مفاهیمی نظیر Export و Import و AggregateCatalog و AssemblyCatalog نیاز است. در صورتی که با این مطالب آشنایی ندارید می‌توانید از (^) شروع کنید.


برای شروع یک پروژه Silverlight ایجاد کنید. بعد از اضافه شدن دو پروژه Silverlight و Web، یک Silverlight Class Library جدید بسازید.
ابتدا یک Page ایجاد کنید و کد‌های زیر را در آن کپی کنید.
<UserControl 
             x:Class="Module1.Module1View1"
             xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" FlowDirection="RightToLeft" FontFamily="Tahoma">
    <StackPanel>
        <sdk:DataGrid Height="100">
            <sdk:DataGrid.Columns>
                <sdk:DataGridTextColumn Header="کد" Width="50"  />
                <sdk:DataGridTextColumn Header="عنوان" Width="200" />
                <sdk:DataGridTextColumn Header="نویسنده" Width="150"  />
            </sdk:DataGrid.Columns>
        </sdk:DataGrid>
        <Button x:Name="NextViewButton"
                Width="150"
                Height="25"
                Foreground="Red"
                Background="Blue"
                Content="لیست طبقه بندی ها" />
    </StackPanel>
</UserControl>
بر روی Page مربوطه راست کلیک کنید و گزینه ViewCode را انتخاب کنید و کد‌های زیر را در آن کپی کنید.
 [Export(typeof(Module1View1))]
    public partial class Module1View1 : UserControl
    {
        [Import]
        public IRegionManager TheRegionManager { private get; set; }

        public Module1View1()
        {
            InitializeComponent();

            NextViewButton.Click += NextViewButton_Click;
        }

        void NextViewButton_Click(object sender, RoutedEventArgs e)
        {
            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View2", UriKind.Relative),
                a => { }
            );
        }
    }
ابتدا خود این View باید حتما Export شود. در رویداد کلیک با استفاده از متد RequestNavigate می‌توانیم به View مورد نظر برای نمایش در Shell اشاره کنیم و این View در Region نمایش داده می‌شود. به دلیل اینکه در این کلاس به RegionManager نیاز داریم از ImportAttribute استفاده کردیم. این بدین معنی است که کلاس Module1View1 وابستگی مستقیم به IRegionManager دارد.

حال یک Page دیگر برای طبقه بندی کتاب‌ها ایجاد کنید و کدهای زیر را در آن کپی کنید.
<UserControl 
             xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"  x:Class="Module1.Module1View2"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" FlowDirection="RightToLeft" FontFamily="Tahoma">
    <StackPanel>
        <sdk:DataGrid Height="100">
            <sdk:DataGrid.Columns>
                <sdk:DataGridTextColumn Header="کد" Width="150"/>
                <sdk:DataGridTextColumn Header="عنوان" Width="150"/>                
            </sdk:DataGrid.Columns>
        </sdk:DataGrid>
        <Button x:Name="NextViewButton"
                Width="150"
                Height="25"
                Foreground="Green"
                Background="Yellow"
                Content="لیست کتاب ها" />
    </StackPanel>
</UserControl>
در Code Behind این Page نیز کد‌های زیر را قرار دهید.
using Microsoft.Practices.Prism.Regions;
using System;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Controls;

namespace Module1
{
    [Export]
    public partial class Module1View2 : UserControl
    {
        IRegion _region1;

        [ImportingConstructor]
        public Module1View2( [Import] IRegionManager regionManager )
        {
            InitializeComponent();
         
            ViewModel viewModel = new ViewModel();
            DataContext = viewModel;

            viewModel.ShouldNavigateFromCurrentViewEvent += () => { return true; };

            _region1 = regionManager.Regions["MyRegion1"];

            NextViewButton.Click += NextViewButton_Click;
        }

        void NextViewButton_Click( object sender, RoutedEventArgs e )
        {
            _region1.RequestNavigate
            (
                new Uri( "Module1View1", UriKind.Relative ),
                a => { }
            );
        }
    }
}
در این ماژول برای اینکه بتوانیم حالت گردشی در فراخوانی ماژول‌ها را داشته باشیم ابتدا DataContext این کلاس را برابر با ViewModel ساخته شده قرار دادیم. با استفاده از رویداد ShoudlNavigateFromCurrentViewEvent که در کلاس ViewModel وجود دارد تعیین می‌کنیم که آیا باید از این View به View قبلی برگشت داشته باشیم یا نه. در صورتی که مقدار false برگشت داده شود خواهید دید که امکان فراخوانی View1 از View2 امکان پذیر نیست. در رویداد کلیک نیز همانند Page قبلی با استفاده از RegionManager و متد RequestNavigate به View مورد نظر راهبری کرده ایم.
نکته: اگر یک کلاس، سازنده با پارامتر داشته باشد باید با استفاده از ImportingConstructor حتما سازنده مورد نظر را هنگام وهله سازی مشخص کنیم در غیر این صورت با Exception مواجه خواهید شد.
حال قصد ایجاد کلاس ViewModel بالا را داریم:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using Microsoft.Practices.Prism.Regions;

namespace Module1
{
    public class ViewModel : IConfirmNavigationRequest
    {       
        public event Func<bool> ShouldNavigateFromCurrentViewEvent;      

        public bool IsNavigationTarget( NavigationContext navigationContext )
        {
            return true;
        }

        public void OnNavigatedTo( NavigationContext navigationContext )
        {

        }

        public void OnNavigatedFrom( NavigationContext navigationContext )
        {

        }         

        public void ConfirmNavigationRequest( NavigationContext navigationContext, Action<bool> continuationCallback )
        {
            bool shouldNavigateFromCurrentViewFlag = false;

            if ( ShouldNavigateFromCurrentViewEvent != null )
                shouldNavigateFromCurrentViewFlag = ShouldNavigateFromCurrentViewEvent();

            continuationCallback( shouldNavigateFromCurrentViewFlag );
        }    
    }
}
توضیح متد‌های بالا:
  • IsNavigateTarget : برای تعیین اینکه آیا کلاس پیاده سازی کننده اینترفیس، می‌تواند عملیات راهبری را مدیریت کند یا نه.
  • OnNavigateTo : زمانی عملیات راهبری وارد View شود(بهتره بگم View مورد نظر در Region صفحه لود شود) این متد فراخوانی می‌شود.
  • OnNavigateFrom : زمانی که راهبری از این View خارج می‌شود (View از حالت لود خارج می‌شود) این متد فراخوانی خواهد شد.
  • ConfirmNavigationRequest : برای تایید عملیات راهبری توسط کلاس پیاده سازی کننده اینترفیس استفاده می‌شود. 
حال یک کلاس برای پیاده سازی و مدیریت ماژول می‌سازیم.
using Microsoft.Practices.Prism.MefExtensions.Modularity;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
using System.ComponentModel.Composition;

namespace Module1
{
    [ModuleExport(typeof(Module1Impl))]
    public class Module1Impl : IModule
    {       
        [Import]
        public IRegionManager TheRegionManager { private get; set; }     

        public void Initialize()
        {
            TheRegionManager.RegisterViewWithRegion("MyRegion1", typeof(Module1View1));

            TheRegionManager.RegisterViewWithRegion("MyRegion1", typeof(Module1View2));
        }
    }
}
همان طور که مشاهده می‌کنید از ModuleExportAttribute برای شناسایی ماژول توسط MefBootstrapper استفاده کردیم و نوع آن را Module1Imp1 قرار دادیم.  ImportAttribute استفاده شده در این کلاس و خاصیت TheRegionManager برای این است که در هنگام ساخت Instance از این کلاس IRegionManager موجود در Container باید در اختیار این کلاس قرار گیرد(نشان دهنده وابستگی مستقیم این کلاس با IRegionManager است). روش دیگر این است که در سازنده این کلاس هم این اینترفیس را تزریق کنیم.
در متد Initialize برای RegionManager دو View ساخته شده را رجیستر کردیم. این کار باید به تعداد View‌های موجود در ماژول انجام شود.
Shell
در پروژه اصلی بک Page به نام Shell ایجاد کنید و کد‌های زیر را در آن کپی کنید.
<UserControl x:Class="NavigationViaViewModel.Shell"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://www.codeplex.com/prism" FlowDirection="RightToLeft" FontFamily="Tahoma">

    <Grid x:Name="LayoutRoot"
          Background="White">
        <TextBlock Text="لیست کتاب‌ها به همراه طبقه بندی آن ها"
                   FontSize="19"
                   Foreground="Black"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Top" />
        <ContentControl HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        prism:RegionManager.RegionName="MyRegion1" />
    </Grid>
</UserControl>
همانند مثال قبلی یک ContentControl داریم و به وسیله RegionName که یک AttachedProperty است یک Region به نام MyRegion1 ایجاد کردیم. تمام ماژول‌های این مثال در این محدوده نمایش داده خواهند شد.
Bootstrapper
حال نیاز به یک Bootstrapper داریم. برای این کار یک کلاس به نام TheBootstrapper بسازید:
using Microsoft.Practices.Prism.MefExtensions;
using Microsoft.Practices.Prism.Modularity;
using System.ComponentModel.Composition.Hosting;
using System.Windows;

namespace NavigationViaViewModel
{
    public class TheBootstrapper : MefBootstrapper
    {
        protected override void InitializeShell()
        {
            base.InitializeShell();

            Application.Current.RootVisual = (UIElement)Shell;
        }

        protected override DependencyObject CreateShell()
        {
            return Container.GetExportedValue<Shell>();
        }

        protected override void ConfigureAggregateCatalog()
        {
            base.ConfigureAggregateCatalog();        
            AggregateCatalog.Catalogs.Add(new AssemblyCatalog(this.GetType().Assembly));
        }

        protected override IModuleCatalog CreateModuleCatalog()
        {
            ModuleCatalog moduleCatalog = new ModuleCatalog();
    
            moduleCatalog.AddModule
            (
                new ModuleInfo
                {
                    InitializationMode = InitializationMode.WhenAvailable,
                    Ref = "Module1.xap",
                    ModuleName = "Module1Impl",
                    ModuleType = "Module1.Module1Impl, Module1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
                }
            );

            return moduleCatalog;
        }
    }
}
متد CreateShell اولین متد در این کلاس است که اجرا خواهد شد. بعد از متد CreateShell، متد InitializeShell اجرا خواهد شد. خاصیت Shell دقیقا به مقدار برگشتی متد CreateShell اشاره خواهد کرد. در متد InitializeShell  مقدار خاصیت Shell به RootVisual این پروژه اشاره می‌کند(مانند MainWindow در کلاس Application پروژه‌های WPF).
متد ConfigureAggregateCatalog برای مدیریت کاتالوگ‌ها و ماژول‌ها که هر کدام در یک اسمبلی جدا وجود خواهند شد استفاده می‌شود. در این متد من از AssemblyCatalog استفاده کردم. AssemblyCatalog  تمام کلاس هایی که ExportAttribute را به همراه دارند شناسایی می‌کند و آن‌ها را در Container نگهداری خواهد کرد(^). مانند یک ServiceLocator در Microsoft unity Service Locator(^) .
متد آخر به نام CreateModuleCatalog است و باید در آن تمام ماژول‌های برنامه را به کلاس ModuleCatalog اضافه کنیم. در مثال پست قبلی به دلیل استفاده از UnityBootstrapper باید این کار را از طریق BuildEvent ‌ها مدیریت می‌کردیم ولی در این جا Mef به راحتی این کار را انجام خواهد داد.
تغییرات زیر را در فایل App.Xaml قرار دهید و پروژه را اجرا کنید.
public partial class App : Application
 {
        public App()
        {
            this.Startup += this.Application_Startup;                 
            InitializeComponent();
        }

        private void Application_Startup(object sender, StartupEventArgs e)
        {
           var bootstrapper = new TheBootstrapper();
            bootstrapper.Run(); 
        }
}
با کلیک بر روی ماژول عملیات راهبری برای ماژول انجام خواهد شد.

دریافت سورس پروژه
ادامه دارد..