مطالب
خواندنی‌های 16 تیر

اس کیوال سرور

توسعه وب

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

دبلیو پی اف و سیلور لایت

سی و مشتقات

شیرپوینت

کتاب‌های رایگان

مای اس کیوال

متفرقه

وب سرورها

پی اچ پی

مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی
فعال سازی تنظیمات مسیریابی

یکی دیگر از تغییرات عمده‌ی ASP.NET Core با نگارش‌های قبلی آن، نحوه‌ی مدیریت مسیریابی‌های سیستم است. در نگارش‌های قبلی مبتنی بر HTTP Moduleها، مسیریابی‌ها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شده‌اند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل می‌شود:


اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کرده‌است:


میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام می‌شود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
 app.UseMvcWithDefaultRoute();
که مطابق سورس ASP.NET Core، معادل است با فراخوانی متد app.UseMvc، با قالب پیش فرضی به صورت زیر:
    public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app)
    {
      if (app == null)
        throw new ArgumentNullException("app");
      return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}")));
    }
قالب مشخص شده‌ی در اینجا به ASP.NET MVC می‌گوید که از کدام قسمت‌های URL باید نام کلاس کنترلر (کلاس ویژه‌ای که به کلمه‌ی Controller ختم می‌شود) و نام اکشن متد متناظر با آن‌را انتخاب کند (اکشن متد، متدی است عمومی در آن کلاس).
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
 [Route("[controller]/[action]")]


مسیریابی‌های قراردادی

در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers
{
  public class HomeController
  {
   public string Index()
   {
    return "Running a POCO controller!";
   }
  }
}
علت کار کردن مسیریابی آن نیز به ذکر متد app.UseMvcWithDefaultRoute در کلاس آغازین برنامه بر می‌گردد و همانطور که عنوان شد، این فراخوانی را می‌توان با فراخوانی واضح‌تر ذیل جایگزین کرد:
app.UseMvc(routes =>
{
  routes.MapRoute(
   name: "default",
   template: "{controller=Home}/{action=Index}/{id?}");
});
پارامتر این متد که جایگزین متد ConfigureRoutes، در نگارش‌های قبلی ASP.NET MVC شده‌است، از نوع IRouteBuilder می‌باشد.
در این تعاریف، هر کدام از قسمت‌های قرارگرفته‌ی داخل {}، مشخص کننده‌ی قسمتی از URL دریافتی بوده و نام‌های controller و action در اینجا جزو نام‌های از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار می‌گیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت می‌شود. همچنین قسمت آخر این پردازش به ?id ختم شده‌است. وجود  ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شده‌اند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش می‌شوند.
در اینجا به هر تعدادی که نیاز است می‌توان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آن‌ها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابی‌های تعریف شده، در همان سطح سبب خاتمه‌ی پردازش سایر مسیریابی‌ها می‌شود.


استفاده از Attributes برای تعریف مسیریابی‌ها

بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، می‌توان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابی‌های قراردادی، از نگارش‌های آغازین ASP.NET MVC به همراه آن بوده‌اند، اما با زیاد شدن تعداد کنترلرها و مسیریابی‌های سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمی‌توان تشخیص داد که چه مسیریابی‌های خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامه‌های بزرگ و ساده سازی تعاریف مسیریابی‌های خاص آن‌ها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شده‌است.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
  public class AboutController : Controller
  {
   public ActionResult Hello()
   {
    return Content("Hello from DNT!");
   }
 
   public ActionResult SiteName()
   {
    return Content("DNT");
   }
  }
}
این کلاس و کنترلر، به صورت پیش فرض نیاز به تعریف هیچ نوع مسیریابی جدیدی ندارد. همان مسیریابی پیش فرض ثبت شده‌ی در کلاس آغازین برنامه، تمام متدهای عمومی آن یا همان اکشن متدهای آن‌را پوشش می‌دهد. برای مثال جهت رسیدن به اکشن متد SiteName آن، می‌توان آدرس /About/SiteName/ را درخواست داد.
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آن‌را با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، می‌توان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
  [Route("About")]
  public class AboutController : Controller
  {
   [Route("")]
   public ActionResult Hello()
   {
    return Content("Hello from DNT!");
   }
 
   [Route("SiteName")]
   public ActionResult SiteName()
   {
    return Content("DNT");
   }
  }
}
در اینجا با اولین Route تعریف شده، مشخص کرده‌ایم که اگر قسمت اول URL درخواستی معادل about بود، پردازش برنامه باید به این کنترلر هدایت شود. بدیهی است الزامی به یکی بودن نام Route، با نام کنترلر، وجود ندارد. همچنین Route تعریف شده‌ی با رشته‌ی خالی، به معنای مسیریابی پیش فرض است. یعنی اگر آدرس /about/ درخواست داده شد، اکشن متد پیش فرض آن، متد Hello خواهد بود. در این حالت، ذکر Route بعدی برای اکشن متد SiteName الزامی است و اگر این‌کار صورت نگیرد، به استثنای ذیل خواهیم رسید:
 AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied:

Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest)
Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
که عنوان کرده‌است در این حالت مشخص نیست که اکشن متد پیش فرض، کدام است.

روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
  [Route("[controller]")]
  public class AboutController : Controller
  {
   [Route("")]
   public ActionResult Hello()
   {
    return Content("Hello from DNT!");
   }
 
   [Route("[action]")]
   public ActionResult SiteName()
   {
    return Content("DNT");
   }
  }
}
عموما مرسوم است که نام مسیریابی کنترلر همان نام کنترلر باشد و نام مسیریابی اکشن متد، همان نام اکشن متد مربوطه. به همین جهت می‌توان از توکن‌های ویژه‌ی [controller] و [action] نیز در اینجا استفاده کرد که دقیقا به همان نام کنترلر و اکشن متد متناظر با آن‌ها تفسیر خواهند شد. مزیت این‌کار این است که در صورت تغییر نام متدها یا کنترلرها، دیگر نیازی نیست تا نام‌های تعریف شده‌ی در ویژگی‌های Route را نیز تغییر داد.

یک نکته: در حین تعریف مسیریابی یک کنترلر می‌توان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
 [Route("api/[controller]")]
وجود api در اینجا به این معنا است که از این پس تنها آدرس /api/about/ پردازش خواهد شد و اگر صرفا آدرس /about/ درخواست شود، با خطای 404 و یا یافت نشد، کار خاتمه می‌یابد.


تعریف قیود، برای مسیریابی‌های تعریف شده

فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت می‌دهد، اضافه کرده‌ایم:
//[Route("/Users/{userid}")]
[Route("Users/{userid}")]
public IActionResult GetUsers(int userId)
{
    return Json(new { userId = userId });
}
در اینجا تعریف مسیریابی آن با users/ و user معانی کاملا متفاوتی را دارند. اگر مسیریابی Users/{userid}/ را تعریف کنیم، یعنی مسیر ذیل از ریشه‌ی سایت باید درخواست شود: http://localhost:7742/users/1
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا می‌کند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشده‌است)؛ مانند:
[Route("/Users/{userid:int?}")]
در اینجا اگر بخواهیم نوع پارامتر درخواستی را نیز دقیقا مشخص و مقید کنیم، می‌توان به صورت ذیل عمل کرد:
 [Route("Users/{userid:int}")]
اگر این کار را انجام ندهیم، با درخواست مسیر http://localhost:7742/dnt/about/users/test مقدار صفر به userId ارسال می‌شود (چون پارامتر test عددی نیست). اگر تنظیم فوق را انجام دهیم، کاربر خطای 404 را دریافت می‌کند.

قیودی را که در اینجا می‌توان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با  (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودی‌های اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشته‌ی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشته‌ی مجاز ورودی.
• range - ذکر بازه‌ی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.

برای ترکیب چندین قید مختلف نیز می‌توان از : استفاده کرد:
 [Route("/Users/{userid:int:max(1000):min(10)}")]


ذکر نام Route برای ساده سازی تعریف آدرسی به آن

در حین تعریف یک Route می‌توان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شده‌ی در کلاس آغازین برنامه):
 [Route("/Users/{userid:int}", Name="GetUserById")]
مزیت آن این است که اکنون برای اشاره‌ی به این مسیریابی خاص می‌توان از این نام تعریف شده استفاده کرد:
 string uri = Url.Link("GetUserById", new { userid = 1 });
پارامتر اول ذکر شده، نام مسیریابی و پارامتر دوم، پارامترهای مرتبط با این مسیریابی هستند.


مشخص سازی ترتیب پردازش مسیریابی‌ها

ترتیب مسیریابی‌های ثبت شده‌ی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آن‌ها است.
در اینجا می‌توان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش می‌شوند (مقدار پیش فرض آن نیز صفر است):
 [Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]


امکان تعریف قیود سفارشی

اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، می‌توان به صورت ذیل عمل کرد:
using System;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
 
namespace Core1RtmEmptyTest
{
  public class CustomRouteConstraint : IRouteConstraint
  {
   public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values,
    RouteDirection routeDirection)
   {
    object value;
    if (!values.TryGetValue(routeKey, out value) || value == null)
    {
      return false;
    }
 
    long longValue;
    if (value is long)
    {
      longValue = (long)value;
      return longValue != 10;
    }
 
    var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
    if (long.TryParse(valueString, NumberStyles.Integer,
      CultureInfo.InvariantCulture, out longValue))
    {
      return longValue != 10;
    }
    return false;
   }
  }
}
در اینجا یک کلاس جدید را که اینترفیس IRouteConstraint را پیاده سازی می‌کند تعریف کرده‌ایم:
public class CustomRouteConstraint : IRouteConstraint
سپس در متد match آن بررسی کرده‌ایم که اگر userid=10 بود، خطای 404 صادر شود.
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
پس از آن، این نام جدید ثبت شده‌ی در اینجا، به نحو ذیل قابل استفاده است:
 [Route("/Users/{userid:int:custom}")]
به این ترتیب userid باید از نوع int بوده و همچنین قید custom را نیز پوشش دهد (یعنی userid=10 نباشد).

یک نکته:  اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شده‌اند.


معرفی بسته‌ی نیوگت Microsoft.AspNetCore.SpaServices

مسیریابی‌های پیش فرض ASP.NET Core با مسیریابی‌های برنامه‌های SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواست‌های رسیده‌ی به سرور، ابتدا به موتور پردازشی ASP.NET وارد می‌شوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمی‌شود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابی‌های موجود اضافه کنند (به آن catch all هم می‌گویند)
app.UseMvc(routes =>
{
  routes.MapRoute(
   name: "default",
   template: "{controller=Home}/{action=Index}/{id?}");
 
  routes.MapRoute(
   name: "spa-fallback",
   template: "{*url}",
   defaults: new { controller = "Home", action = "Index" });
});
در اینجا هر درخواستی که با مسیریابی default تطابق نداشت، توسط الگوی عمومی {url*} پردازش می‌شود و این پردازش در نهایت سبب راه اندازی برنامه‌ی SPA می‌گردد. اما مشکل اینجا است که برای فایل‌های استاتیک غیرموجود مانند تصاویر، فایل‌های js و css نیز خروجی HTML ایی خواهیم داشت؛ بجای خروجی 404 و یافت نشد.
برای حل این مشکل مایکروسافت بسته‌ای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینه‌ی manage nuget packages را انتخاب کنید. سپس در برگه‌ی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:


انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{
    "dependencies": {
       //same as before  
       "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007"
 },
پس از بازیابی و نصب آن، اکنون catch all را حذف کرده و با یک سطر routes.MapSpaFallbackRoute ذیل جایگزین کنید:
app.UseMvc(routes =>
{
  routes.MapRoute(
   name: "default",
   template: "{controller=Home}/{action=Index}/{id?}");
 
  routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" });
});
و برای یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک» در AngularJS 2.0، علاوه بر عمومی کردن پوشه‌ی wwwroot توسط UseFileServer نیاز است پوشه‌ی node_modules را هم با تنظیمات ذیل عمومی کرد و در معرض دید عموم قرار داد (جایی که بسته‌های node.js نصب می‌شوند):
// Serve wwwroot as root
app.UseFileServer();
 
// Serve /node_modules as a separate root (for packages that use other npm modules client side)
app.UseFileServer(new FileServerOptions
{
  // Set root of file server
  FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")),
  // Only react to requests that match this path
  RequestPath = "/node_modules",
  // Don't expose file system
  EnableDirectoryBrowsing = false
});
مطالب دوره‌ها
استفاده از Async و Await در برنامه‌های دسکتاپ
امکان استفاده از قابلیت‌های غیرهمزمان دات نت 4.5 در برنامه‌های WPF نیز به روش‌های مختلفی میسر است که در ادامه دو روش مرسوم آن‌را بررسی خواهیم کرد.

تهیه مقدمات بحث

ابتدا یک برنامه‌ی WPF جدید را آغاز کنید. سپس کدهای MainWindow.xaml آن‌را به نحو ذیل تغییر دهید.
<Window x:Class="Async10.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <DockPanel Dock="Top">
            <Button Name="BtnGo" Content="Go" Click="BtnGo_OnClick" />
            <ProgressBar Name="ProgressBar" IsIndeterminate="True" Visibility="Collapsed"/>
        </DockPanel>
        <TextBox Name="Results"/>
    </DockPanel>
</Window>
قصد داریم اطلاعاتی را از وب دریافت و سپس در TextBox قرار گرفته در صفحه نمایش دهیم.
در این مثال از کلاس جدید HttpClient نیز استفاده خواهیم کرد. برای استفاده از آن نیاز است ارجاعی را به اسمبلی استاندارد System.Net.Http.dll نیز به پروژه اضافه کنید.


روش اول

در ادامه کدهای فایل MainWindow.xaml.cs را به نحو ذیل تغییر داده و سپس برنامه را اجرا کنید.
using System.Net.Http;
using System.Windows;

namespace Async10
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void BtnGo_OnClick(object sender, RoutedEventArgs e)
        {
            BtnGo.IsEnabled = false;
            ProgressBar.Visibility = Visibility.Visible;

            var url = "https://www.dntips.ir";
            var client = new HttpClient(); // make sure you have an assembly reference to System.Net.Http.dll
            client.DefaultRequestHeaders.UserAgent.ParseAdd("Test Async");
            var task = client.GetStringAsync(url);
            task.ContinueWith(t =>
            {
                Results.Text = t.Result;

                BtnGo.IsEnabled = true;
                ProgressBar.Visibility = Visibility.Collapsed;
            });
        }
    }
}
روال رخدادگردان BtnGo_OnClick به نحو مرسوم آن نوشته شده است. بنابراین جهت دریافت نتیجه‌ی متد GetStringAsync می‌توان از متد ContinueWith بر روی task دریافت اطلاعات از وب، استفاده کرد. همچنین در اینجا مستقیما اطلاعات و نتیجه‌ی دریافتی را به عناصر UI انتساب داده‌ایم.
اگر پروژه را اجرا کنید، برنامه با استثنای زیر متوقف می‌شود:
 The calling thread cannot access this object because a different thread owns it.
چون task آغاز شده در ترد دیگری نسبت به ترد UI اجرا می‌شود، مجوز تغییری را در کدهای UI ندارد. برای حل این مشکل می‌توان از دو روش ذیل استفاده کرد:

الف) با استفاده از SynchronizationContext.Current و متد Post آن
 var context = SynchronizationContext.Current;
task.ContinueWith(t => context.Post(state =>
{
   Results.Text = t.Result;

   BtnGo.IsEnabled = true;
   ProgressBar.Visibility = Visibility.Collapsed;
}, null));
با این روش در قسمت اول آشنا شدید. SynchronizationContext.Current در اینجا چون در ابتدای متد و خارج از ContinueWith دریافت اطلاعات، اجرا می‌شود، به ترد UI یا ترد اصلی برنامه اشاره می‌کند. سپس همانطور که ملاحظه می‌کنید، توسط متد Post آن می‌توان اطلاعات را در زمینه‌ی تردی که SynchronizationContext به آن اشاره می‌کند اجرا کرد.

ب) با استفاده از امکانات TaskScheduler
 var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
task.ContinueWith(t =>
{
   Results.Text = t.Result;

   BtnGo.IsEnabled = true;
   ProgressBar.Visibility = Visibility.Collapsed;
}, taskScheduler);
وقتی یک task اجرا می‌شود، TPL یا task parallel library نیاز دارد بداند، این task بر روی چه تردی و چه زمانی قرار است اجرا شود. به صورت پیش فرض از thread pool استفاده می‌کند، اما الزامی به آن نیست. با استفاده از TaskScheduler می‌توان بر روی نحوه‌ی رفتار تردهای TPL تاثیر گذاشت و یا حتی آن‌ها را سفارشی سازی کرد. متد FromCurrentSynchronizationContext، یک TaskScheduler جدید را در اختیار ما قرار می‌دهد که کدهای آن بر اساس SynchronizationContext.Current کار می‌کند؛ در اینجا Context به UI اشاره می‌کند و در یک برنامه‌ی وب، به یک درخواست رسیده.
برای مثال اگر در برنامه‌های وب یک Task جدید را اجرا کنید شاید اینطور به نظر برسد که به HttpContext دسترسی ندارید. این نقیصه را می‌توان توسط کار با SynchronizationContext جاری برطرف کرد.
در مثال فوق، چون taskScheduler پیش از فراخوانی متد ContinueWith ایجاد شده‌است، به ترد UI اشاره می‌کند. در این حالت برای نمایش اطلاعات در همان ترد اصلی برنامه کافی است این taskScheduler را به عنوان پارامتر متد ContinueWith معرفی کنیم.


روش دوم

در دات نت 4.5 می‌توان روال رخدادگردان تعریف شده را به صورت async نیز معرفی کرد (یعنی مجاز هستیم امضای متد پیش فرض تولید شده را تغییر دهیم):
 private async void BtnGo_OnClick(object sender, RoutedEventArgs e)
سپس استفاده از await در کدهای برنامه میسر خواهد شد:
        private async void BtnGo_OnClick(object sender, RoutedEventArgs e)
        {
            BtnGo.IsEnabled = false;
            ProgressBar.Visibility = Visibility.Visible;

            var url = "https://www.dntips.ir";
            var client = new HttpClient(); // make sure you have an assembly reference to System.Net.Http.dll
            client.DefaultRequestHeaders.UserAgent.ParseAdd("Test Async");
            Results.Text = await client.GetStringAsync(url);
            BtnGo.IsEnabled = true;
            ProgressBar.Visibility = Visibility.Collapsed;
        }
در این حالت دیگر نیازی به استفاده از ContinueWith و مباحث SynchronizationContext نیست. زیرا تمام آن‌ها به صورت توکار اعمال می‌شوند. به علاوه کدنهایی نیز بسیار خواناتر شده‌است.
مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت هشتم - ساده سازی معرفی سرویس‌ها توسط Scrutor
قابلیت‌های قرار گرفته‌ی در اسمبلی Microsoft.Extensions.DependencyInjection که پایه‌ی تزریق وابستگی‌های برنامه‌های مبتنی بر NET Core. را ارائه می‌دهد، برای پیاده سازی اکثر پروژه‌ها کافی است. اما اگر از نگارش‌های پیشین ASP.NET MVC به ASP.NET Core مهاجرت کرده باشید، حتما با قابلیت‌های ویژه‌ی اسکن اسمبلی‌های موجود در IoC Containers ثالث، جهت ساده سازی معرفی سرویس‌های برنامه به سیستم تزریق وابستگی‌ها، آشنایی دارید. برای مثال StructureMap قابلیت اسکن اسمبلی‌های موجود در برنامه و معرفی اینترفیس‌ها و سرویس‌های موجود در آن‌را به Container خود دارد:
var container = new Container(x =>
            {
                x.Scan(scanner =>
                {
                    scanner.AssemblyContainingType<IOrderHandler>();
                    // connects `IAccounting` to `Accounting` and `ISales` to `Sales` automatically.
                    scanner.WithDefaultConventions();
                });
            });
و یا AutoFac نیز به همین صورت:
builder.RegisterAssemblyTypes(myAssembly)
    .Where(t => t.IsAssignableTo<IMyInterface>())
    .AsImplementedInterfaces();
البته می‌توان مجددا به تمام این قابلیت‌ها رسید؛ به شرطی‌که سیستم تزریق وابستگی‌های پایه‌ی NET Core. را با یکی از IoC Containers ثالث به طور کامل تعویض کنیم. اگر قصد چنین تعویض پایه‌ای را ندارید و هنوز قصد دارید از همان Microsoft.Extensions.DependencyInjection استفاده کنید، اما تعدادی متد الحاقی جدید تعریف شده‌ی بر فراز آن، کار اسکن کردن اسمبلی‌ها را مانند قبل انجام دهند، می‌توان از کتابخانه‌ی کمکی Scrutor استفاده کرد. این کتابخانه، جایگزین سیستم تزریق وابستگی‌های توکار برنامه‌های NET Core. نیست؛ بلکه صرفا مکمل آن است.


دریافت و نصب کتابخانه‌ی کمکی Scrutor

کتابخانه‌ی کمکی Scrutor سورس باز بوده و بسته‌ی NuGet آن توسط یکی از دستورات زیر به پروژه افزوده می‌شود:
> Install-Package Scrutor
> dotnet add package Scrutor
و یا به صورت مدخلی جدید در فایل csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Scrutor" Version="3.0.2" />
  </ItemGroup>
</Project>


ثبت و معرفی ساده‌تر سرویس‌ها بر اساس قواعد نامگذاری آن‌ها توسط Scrutor

فرض کنید تعدادی سرویس را به صورت زیر تعریف کرده‌اید:
namespace CoreIocServices
{
    public interface IFoo
    {
        void Run();
    }

    public class Foo : IFoo
    {
        public void Run()
        {
            throw new System.NotImplementedException();
        }
    }

    public interface IBar
    {
        void Add();
    }

    public class Bar : IBar
    {
        public void Add()
        {
            throw new System.NotImplementedException();
        }
    }


    public interface IBaz
    {
        void Stop();
    }

    public class Baz : IBaz
    {
        public void Stop()
        {
            throw new System.NotImplementedException();
        }
    }
}
روش متداول معرفی آن‌ها به IoC Container برنامه به صورت زیر است:
services.AddScoped<IFoo, Foo>();
services.AddScoped<IBar, Bar>();
services.AddScoped<IBaz, Baz>();
و هرچقدر تعداد سرویس‌های برنامه بیشتر شود، سطرهای فوق نیز بیشتر خواهند شد.
در اینجا در حین تعریف سرویس‌های فوق این روش نامگذاری رعایت شده‌است: هر اینترفیس، نامش یک I بیشتر از نام کلاس مشتق شده‌ی از آن دارد؛ مانند اینترفیس IFoo و کلاس Foo. کتابخانه‌ی StructureMap که در ابتدای بحث معرفی شد، کار اسکن و اتصال یک چنین سرویس‌هایی را با تعریف scanner.WithDefaultConventions انجام می‌دهد. معادل آن با Scrutor به صورت زیر است:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Scan(scan =>
                //scan.FromCallingAssembly()
                scan.FromAssemblyOf<IFoo>()
                    .AddClasses()
                    .AsMatchingInterface()
                    .WithScopedLifetime());
تعریف فوق به این معنا است:
- scan.FromAssemblyOf کار اسکن اسمبلی را انجام می‌دهد که نوع IFoo در آن قرار دارد. اگر از scan.FromCallingAssembly استفاده کنیم، به این معنا است که کار اسکن را دقیقا از همین اسمبلی فراخوان کدهای جاری، شروع کن. اما چون IFoo تعریف شده، در یک پروژه و اسمبلی دیگر قرار دارد، به همین جهت نیاز به ذکر صریح اسمبلی آن نیز هست.
- AddClasses یعنی تمام کلاس‌های public, non-abstract را به لیست services اضافه کن.
- AsMatchingInterface یعنی بر اساس قرارداد نامگذاری IClassName و ClassName، اتصالات سرویس‌ها را انجام بده.
بجای آن می‌توان از AsImplementedInterfaces نیز استفاده کرد. این حالت برای زمانی مناسب است که یک کلاس، چندین اینترفیس را پیاده سازی کند (مثلا کلاس TestService اینترفیس‌های ITestService و IService را پیاده سازی کرده باشد) و علاقمند باشید به ازای هر اینترفیس، یکبار سرویس آن نیز ثبت شود؛ کاری مانند تنظیمات زیر:
services.AddScoped<ITestService, TestService>();
services.AddScoped<IService, TestService>();
یا حتی می‌توان از متد ()<As<T نیز استفاده کرد. در اینجا به Scrutor گفته می‌شود که تمام کلاس‌های یافت شده را بر اساس نوع سرویس T ثبت و معرفی کن. البته اگر کلاسی نتواند نوع اینترفیس T را پیاده سازی کند، در زمان اجرا با استثناء مواجه خواهید شد.
- WithScopedLifetime نیز طول عمر این سرویس‌های اضافه شده را مشخص می‌کند. در اینجا می‌توان WithTransientLifetime و WithSingletonLifetime را نیز ذکر کرد.

بنابراین همانطور که ملاحظه می‌کنید، هنوز هم همان سیستم Microsoft.Extensions.DependencyInjection برقرار است؛ اما با وجود متد الحاقی جدید Scan، کار تعاریف سرویس‌های برنامه به شدت ساده می‌شود.


کار با وهله‌های کلاس‌های سرویس‌ها بجای اینترفیس‌های آن توسط Scrutor

می‌خواهیم مثال سوم قسمت ششم «چگونه بجای اینترفیس‌ها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگی‌ها درخواست کنیم؟» را توسط Scrutor پیاده سازی کنیم:
namespace CoreIocServices
{
    public interface IService { }
    public class Service1 : IService { }
    public class Service2 : IService { }
    public class Service : IService { }
}
در حالت متداول آن می‌توان از روش زیر نیز استفاده کرد:
services.AddTransient<Service1>();
services.AddTransient<Service2>();
services.AddTransient<Service>();
که با افزایش تعداد کلاس‌های سرویس برنامه به همین نحو نیز افزایش خواهند یافت. معادل این تنظیمات با Scrutor به صورت زیر است:
namespace CoreIocSample02
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.Scan(scan =>
              //scan.FromCallingAssembly()
              scan.FromAssemblyOf<IService>()
                  .AddClasses()
                  .AsSelf()
                  .WithTransientLifetime());
در اینجا اسمبلی حاوی IService اسکن خواهد شد و سپس تمام کلاس‌های public, non-abstract آن AsSelf (ثبت پیاده سازی خود کلاس به عنوان سرویس) با طول عمر Transient به لیست services اضافه می‌شوند و یا اگر صرفا تعدادی سرویس مشخص مد نظر بود می‌توان به صورت زیر عمل کرد:
services.Scan(scan =>
               scan.AddTypes(new[] { typeof(Service1), typeof(Service2) })
                   .AsSelf()
                   .WithTransientLifetime());
متدهایی که در Scrutor، یک پیاده سازی را به عنوان سرویس معرفی می‌کنند، شامل این موارد هستند:
AsSelf: معادل ()<services.AddTransient<TestService است. در این حالت کلاس‌هایی که اینترفیسی را پیاده سازی نمی‌کنند و یا در کل مایل هستید که از طریق تزریق وابستگی‌ها در دسترس باشند، می‌توان توسط متد AsSelf به سیستم معرفی کرد.
AsSelfWithInterfaces: معادل تنظیمات زیر است:
services.AddSingleton<TestService>();
services.AddSingleton<ITestService>(x => x.GetRequiredService<TestService>());
services.AddSingleton<IService>(x => x.GetRequiredService<TestService>());
فرض کنید کلاس TestService اینترفیس‌های ITestService و IService را پیاده سازی کرده باشد. با استفاده از AsSelfWithInterfaces، یکبار پیاده سازی خود سرویس به سیستم معرفی می‌شود، سپس به ازای هر اینترفیس، از همان وهله‌ی TestService برای وهله سازی سرویس‌های ITestService و IService نیز استفاده می‌شود.


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

Scrutor به همراه روش‌های متعددی برای تعریف اسمبلی یا اسمبلی‌هایی است که باید اسکن شوند و نمونه‌ای از آن‌را با FromAssemblyOf بررسی کردیم:
services.Scan(scan =>
              //scan.FromCallingAssembly()
              scan.FromAssemblyOf<IService>()
سایر موارد آن به شرح زیر هستند:
الف) FromAssemblyOf<>, FromAssembliesOf : اسمبلی یا اسمبلی‌هایی که نوع یا نوع‌های تعیین شده را به همراه دارند، اسکن می‌کند.
ب) FromCallingAssembly, FromExecutingAssembly, FromEntryAssembly کار اسکن اسمبلی‌های فراخوان، اسمبلی که هم اکنون در حال اجرا است و اسمبلی آغازین برنامه را انجام می‌دهند.
ج) FromAssemblyDependencies: تمام اسمبلی‌هایی را که وابسته‌ی به اسمبلی معرفی شده‌ی به آن هستند، اسکن می‌کند.
د) FromApplicationDependencies, FromDependencyContext: تمام اسمبلی‌هایی را که توسط برنامه، ارجاعی به آن‌ها وجود دارند، اسکن می‌کند.


انتخاب دقیق‌تر کلاس‌ها و سرویس‌های مدنظر توسط Scrutor

شاید عملکرد کلی متد AddClasses مدنظر شما نباشد و نیاز به انتخاب دقیق‌تری از سرویس‌های اسکن شده را داشته باشید؛ برای این مورد نیز Scrutor روش‌های زیر را ارائه می‌دهد. برای مثال خود کلاس AddClasses دارای overloadهای زیر نیز هست:
    public interface IImplementationTypeSelector : IAssemblySelector, IFluentInterface
    {
        IServiceTypeSelector AddClasses();
        IServiceTypeSelector AddClasses(bool publicOnly);
        IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action);
        IServiceTypeSelector AddClasses(Action<IImplementationTypeFilter> action, bool publicOnly);
    }
حالت پیش‌فرض آن انتخاب تمام کلاس‌های public, non-abstract است. اگر پارامتر publicOnly را با false مقدار دهی کنید، internal/private nested classes را نیز انتخاب می‌کند. پارامتر action ای که در اینجا درنظر گرفته شده، جهت فیلتر کردن سرویس‌های انتخابی است که تعدادی از مثال‌های آن‌را در زیر بررسی می‌کنیم:
services.Scan(scan => scan
              .FromAssemblyOf<IService>()
                .AddClasses(classes => classes.AssignableTo<IService>())
// .AddClasses(classes => classes.InNamespaces("MyApp")) 
// .AddClasses(classes => classes.Where(type => type.Name.EndsWith("Repository")) 
                    .AsImplementedInterfaces()
                    .WithTransientLifetime());
در اینجا در حالت اول، کلاس‌هایی که صرفا اینترفیس IService را پیاده سازی کرده باشند، انتخاب می‌شوند. حالت دوم آن، انتخاب‌ها را به یک فضای نام محدود می‌کند و حالت سوم اگر نام کلاسی به Repository ختم شود، آن‌را به عنوان سرویس انتخاب خواهد کرد.


مدیریت جایگزینی سرویس‌ها توسط Scrutor

یکی از مزیت‌های طراحی یک برنامه با درنظر گرفتن الگوی تزریق وابستگی‌ها، امکان جایگزین کردن سرویس‌های پیش‌فرض آن با سرویس‌های دیگری است. فرض کنید کتابخانه‌ای ارائه شده و از الگوریتم هش کردن X استفاده کرده‌است؛ اما شما علاقمندید تا از الگوریتم Y بجای آن استفاده کنید. اگر این کتابخانه وهله‌ی الگوریتم هش کردن را از طریق تزریق وابستگی‌ها تامین کرده باشد، فقط کافی است در ابتدای معرفی تنظیمات تزریق وابستگی‌های آن، سرویس الگوریتم هش کردن موجود را با نمونه‌ی خاص خودتان جایگزین کنید.
اکنون فرض کنید پیش از استفاده‌ی از Scrutor، تعدادی سرویس را به روش متداولی ثبت و معرفی کرده‌اید:
services.AddTransient<ITransientService, TransientService>();
services.AddScoped<IScopedService, ScopedService>();
حال که قرار است متد Scan آن، سرویس‌های یک اسمبلی را به لیست موجود اضافه کند، به سرویس‌های زیر می‌رسد:
public class TransientService : IFooService {}
public class AnotherService : IScopedService {}
 رفتار آن با سرویس‌های معادلی که از پیش ثبت شده‌اند چگونه باید باشد؟ برای مدیریت این مساله، متد UsingRegistrationStrategy پیش بینی شده‌است:
services.Scan(scan =>
                scan.FromAssemblyOf<IFoo>()
                    .AddClasses()
                    .UsingRegistrationStrategy(RegistrationStrategy.Skip)
                    .AsMatchingInterface()
                    .WithScopedLifetime());
و پارامتر دریافتی آن یک چنین امضایی را دارد:
namespace Scrutor
{
    public abstract class RegistrationStrategy
    {
        public static readonly RegistrationStrategy Skip;
        public static readonly RegistrationStrategy Append;
        protected RegistrationStrategy();
        public static RegistrationStrategy Replace();
        public static RegistrationStrategy Replace(ReplacementBehavior behavior);
        public abstract void Apply(IServiceCollection services, ServiceDescriptor descriptor);
    }
}
- حالت Append آن که حالت پیش‌فرض نیز هست، تمام سرویس‌های یافت شده را به لیست IServiceCollection اضافه می‌کند؛ صرفنظر از اینکه پیشتر ثبت شده‌است یا خیر.
- حالت Skip آن، سرویسی را تکراری ثبت نمی‌کند. یعنی اگر سرویسی پیشتر در مجموعه‌ی IServiceCollection موجود بود، مجددا آن‌را ثبت نمی‌کند.

سپس نوبت به متدهای Replace می‌رسد که یک چنین پارامتری را قبول می‌کنند:
namespace Scrutor
{
    [Flags]
    public enum ReplacementBehavior
    {
        Default = 0,
        ServiceType = 1,
        ImplementationType = 2,
        All = 3
    }
}
- در حالت استفاده‌ی از Replace(​ReplacementBehavior.​ServiceType)، اگر سرویسی پیشتر در لیست IServiceCollection ثبت شده باشد، آن‌را حذف کرده و سپس نمونه‌ی جدید را ثبت می‌کند (ثبت سرویس بر اساس اینترفیس و پیاده سازی آن).
- در حالت استفاده‌ی از Replace(​ReplacementBehavior.​ImplementationType)، اگر پیاده سازی کلاسی پیشتر در لیست IServiceCollection ثبت شده باشد، آن‌را حذف کرده و سپس نمونه‌ی جدید را ثبت می‌کند (ثبت سرویس صرفا بر اساس نام کلاس آن).
- حالت Replace(​ReplacementBehavior.All) هر دو حالت قبل را با هم شامل می‌شود.


امکان ترکیب چندین استراتژی جستجو با هم توسط Scrutor

در یک برنامه‌ی واقعی غیرممکن است که بخواهید تمام کلاس‌ها را با یک طول عمر، اسکن و ثبت کنید. برای این منظور می‌توان از قابلیت فیلتر کردن کلاس‌ها که در مورد آن بحث شد و همچنین امکان ترکیب زنجیر وار حالت‌های مختلف اسکن، استفاده کرد:
services.Scan(scan => scan 
  .FromAssemblyOf<CombinedService>() 
    .AddClasses(classes => classes.AssignableTo<ICombinedService>()) // Filter classes 
      .AsSelfWithInterfaces() 
      .WithSingletonLifetime() 
 
    .AddClasses(x=> x.AssignableTo(typeof(IOpenGeneric<>))) // Can close generic types 
      .AsMatchingInterface() 
 
    .AddClasses(x=> x.InNamespaceOf<MyClass>()) 
      .UsingRegistrationStrategy(RegistrationStrategy.Replace()) // Defaults to ReplacementBehavior.ServiceType 
      .AsMatchingInterface() 
      .WithScopedLifetime() 
 
  .FromAssemblyOf<DatabaseContext>()   // Can load from multiple assemblies within one Scan() 
    .AddClasses()  
      .AsImplementedInterfaces() 
);
مطالب
جزئیات برنامه نویسی افزونه فارسی به پارسی

این افزونه با استفاده از ابزار Visual Studio Tools for Office که به VSTO مشهور شده است، تهیه شد. در بسته به روز رسانی سیستم که در ذیل (معرفی افزونه) نیز معرفی شد نگارش sp1 vsto3.0 آن به صورت خودکار نصب خواهد شد.
برای ایجاد این پروژه در VS.Net 2008 ، تنها کافی است یک پروژه جدید Word add-in را آغاز نمائیم. (شکل زیر)





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




و خوشبختانه این بانک اطلاعاتی و محصور کننده‌های آن با اطلاعات یونیکد فارسی هیچ مشکلی ندارند و برای کارهایی با وسعت کم و تعداد رکورد پائین یکی از بهترین انتخاب‌ها به‌شمار می‌روند.
نحوه استفاده از SQLite نیز در دات نت بسیار ساده است. اگر با ADO.Net کار کرده باشید، پس از افزودن ارجاعی از اسمبلی System.Data.SQLite.DLL به پروژه و معرفی فضای نام آن به پروژه، تنها کافی است در کدهای قبلی خود برای مثال SqlConnection را به SQLiteConnectionتغییر دهید و امثال آن. یعنی دانش ADO.Net شما در اینجا نیز کاملا قابل استفاده خواهد بود و نیازی نیست مدتی را صرف آشنا شدن با کلاس‌ها و مفاهیم جدید نمائید (البته این تنها زمانی معنا خواهد داشت که به ویزاردها عادت نکرده باشید و کارهای خود را با کد نویسی انجام داده باشید).
تنها یک نکته را باید به‌خاطر داشت و آن هم مربوط است به ساز و کار درونی SQLite . هنگام انجام عملیات update یا insert حتما از transaction استفاده کنید تا سرعت کوئری‌های شما در SQLite به نحو شگفت انگیزی افزایش یابد. مثالی در این مورد را در فایل chm راهنمای SQLite.NET می‌توانید پیدا کنید.

مطلب دیگری که پیش از پرداختن به کد نویسی افزونه باید با آن آشنا شویم، مفهوم smart tags در مجموعه آفیس است که در این پروژه از آن استفاده گردید.
smart tags در مجموعه آفیس برچسب‌هایی هستند که به صورت خودکار توسط یکی از محصولات آفیس مثلا ورد یا اکسل و امثال آن، پس از تشخیص یک کلمه خاص ایجاد می‌شوند و می‌توان اعمالی را به این برچسب ایجاد شده انتساب داد. برای مثال در اینجا امکان جایگزین کردن کلمه فارسی با معادل پارسی در نظر گرفته شد.
ویدیویی در مورد نحوه ایجاد اسمارت تگ‌ها در VS.Net و یا مثالی پیشرفته‌تر در مورد تشخیص دمای فارنهایت در یک متن و ایجاد smart tag مخصوص به آن برای تبدیل به سلسیوس. (از regular expressions جهت یافتن یک الگو در متن استفاده شده است)

در این پروژه، حدود 3800 واژه فارسی به‌ یک smart tag انتساب داده می‌شود (در روال استاندارد ThisAddIn_Startup). سپس در هنگام نمایش آن، معادل پارسی کلمه نیز به منوی باز شده افزوده گشته و در روال رخداد کلیک آن، تعویض کلمه تشخیص داده شده با واژه پیدا شده صورت خواهد گرفت.

در ادامه فرض بر این است که یک پروژه جدید word add-in را در VS.Net ایجاد کرده‌اید و همچنین ارجاعی را به فایل System.Data.SQLite.DLL افزوده‌اید.

using System;
using System.Diagnostics;
using Microsoft.Office.Tools.Word;
using Action = Microsoft.Office.Tools.Word.Action;

private SmartTag _st;
private void init()
{
try
{
//Enable Smart Tags in Word
if (!Application.Options.LabelSmartTags)
{
//ممکن است اسمارت تگ‌ها در ورد غیرفعال باشند. به این صورت می‌شود آنها را فعال کرد
Application.Options.LabelSmartTags = true;
}

_st = new SmartTag(@"www.microsoft.com/Demo#FarsiSmartTag", @"فارسی به پارسی");

//دریافت واژه‌های فارسی از دیتابیس و افزودن خودکار آنها به اسمارت تگ‌ها
if (!DBhelper.AddSmartTagItems(_st, "select distinct farsi from tblFarsiToParsi")) return;

Action stActions = new Action("تبدیل");//تعریف یک اکشن جدید
stActions.Click += stActions_Click;//انتساب روال‌های رخداد گردان
stActions.BeforeCaptionShow += stActions_BeforeCaptionShow;
_st.Actions = new[] { stActions };
VstoSmartTags.Add(_st);//افزودن اسمارت تگ به مجموعه
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

private void ThisAddIn_Startup(object sender, EventArgs e)
{
init();
}

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

static void stActions_BeforeCaptionShow(object sender, ActionEventArgs e)
{
try
{
Action clickedAction = sender as Action;
if (clickedAction != null)
{
string parsi = DBhelper.FindParsi(e.Text);//معادل پارسی از دیتابیس دریافت می‌شود
clickedAction.Caption = (parsi == string.Empty ? e.Text : parsi);
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

static void stActions_Click(object sender, ActionEventArgs e)
{
try
{
Action clickedAction = sender as Action;
if (clickedAction != null)
{
e.Range.Text = clickedAction.Caption;//جایگزینی متن موجود با عنوانی که پیشتر پارسی شده است
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex.ToString(), EventLogEntryType.Error, 7);
}
}

نکته‌ای را که در اینجا باید حتما رعایت کرد بحث exception handling‌ است. خصوصا در روال استاندارد ThisAddIn_Startup . اگر در این روال خطایی مدیریت نشده رخ دهد، word افزودنی شما را به صورت غیرفعال به مجموعه اضافه خواهد کرد و فعال سازی بعدی آن پس از اصلاح کد واقعا مشکل خواهد بود. همانطور که ملاحظه می‌کنید تمامی خطاها در event log‌ ویندوز نوشته می‌شوند.
همچنین باید دقت داشت که اگر متغیری در سطح کلاس تعریف نشود به احتمال زیاد تا دقایقی بعد توسط garbage collector به دیار باقی خواهد شتافت (تعریف st_ در اینجا). اینجاست که شاید ساعت‌ها وقت صرف کنید که چرا روال‌های رخ‌داد گردان دیگر اجرا نمی‌شوند. چرا افزونه دیگر کار نمی‌کند.

همین! کل سورس این add-in منهای بحث دریافت اطلاعات از دیتابیس همین بود! وظیفه‌ی تشخیص کلمات معرفی شده به ms-word به‌عهده‌ی خود آن است و این‌کار را نیز به‌خوبی انجام می‌دهد. در گذشته‌های نچندان دور ایجاد یک افزونه برای word واقعا مشکل بود که با این روش بسیاری از موانع برطرف شده است.

کلاس DBHelper که کار دریافت اطلاعات واژه‌ها را از دیتابیس SQLite انجام می‌دهد به شرح زیر است:

using System;
using System.Data.SQLite;
using System.Diagnostics;
using System.Reflection;
using Microsoft.Office.Tools.Word;

namespace Farsi2Parsi
{
class DBhelper
{
#region Methods (2)

// Public Methods (2)

public static bool AddSmartTagItems(SmartTag st, string strSQL)
{
SQLiteDataReader myReader = null;
SQLiteCommand sqlCmd = null;
bool ret = false;
try
{
SQLiteConnection sqlCon = new SQLiteConnection
{
ConnectionString = "Data Source=" + ConStr.ConnectionString
};
sqlCon.Open();
sqlCmd = new SQLiteCommand(strSQL, sqlCon);
myReader = sqlCmd.ExecuteReader();

if (myReader != null)
while (myReader.Read())
{
if (myReader.GetValue(0) != DBNull.Value)
st.Terms.Add(myReader.GetValue(0).ToString());
}

ret = true;
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex + "\n" + Environment.CurrentDirectory + "\n" +
Assembly.GetExecutingAssembly().Location, EventLogEntryType.Error, 7);
}
finally
{
if (myReader != null)
myReader.Close();

if (sqlCmd != null)
sqlCmd.Connection.Close();
}
return ret;
}

public static string FindParsi(string farsi)
{
SQLiteDataReader myReader = null;
SQLiteCommand sqlCmd = null;
string ret = string.Empty;
string strSQL = "select parsi from tblFarsiToParsi where farsi='" + farsi.Replace("'", "''") + "'";
try
{
SQLiteConnection sqlCon = new SQLiteConnection
{
ConnectionString = "Data Source=" + ConStr.ConnectionString
};
sqlCon.Open();
sqlCmd = new SQLiteCommand(strSQL, sqlCon);
myReader = sqlCmd.ExecuteReader();

if (myReader != null)
{
myReader.Read(); //اولین مورد کافی است
if (myReader.GetValue(0) != DBNull.Value)
ret = myReader.GetValue(0).ToString();
}
}
catch (Exception ex)
{
EventLog.WriteEntry("FarsiToParsi", ex + "\n" + Environment.CurrentDirectory + "\n" +
Assembly.GetExecutingAssembly().Location, EventLogEntryType.Error, 8);
}
finally
{
if (myReader != null)
myReader.Close();

if (sqlCmd != null)
sqlCmd.Connection.Close();
}
return ret;
}
#endregion Methods
}
}

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

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

class ConStr
{
public static string ConnectionString
{
get
{
return Microsoft.Win32.Registry.LocalMachine.OpenSubKey("SOFTWARE\\FarsiToParsi").GetValue("folder") + "\\ErrorsBank.sqlite";
}
}
}

سورس کامل این افزونه را به صورت یک پروژه VS.Net 2008 SP1 از اینجا می‌توانید دریافت کنید.
نصاب برنامه با استفاده از NSIS ایجاد شده که در روزی دیگر درباره‌ی آن توضیح خواهم داد.
اگر قصد داشته باشید از روش‌های متداول استفاده کنید، مشاهده ویدیوی زیر توصیه می‌شود:
http://msdn.microsoft.com/en-us/office/bb851702.aspx

برای توزیع این نوع افزونه‌ها علاوه بر دات نت فریم ورک، به چهار به روز رسانی دیگر نیز نیاز خواهد بود:
به روز رسانی نصاب ویندوز (که احتمالا نصب هست)
WindowsInstaller-KB893803-v2-x86.exe
Microsoft Office System Update: Redistributable Primary Interop Assemblies :
o2007pia.msi
نصب vsto و همچنین sp1 آن
vstor30.exe
vstor30sp1-KB949258-x86.exe

این موارد را من در بسته به روز رسانی سیستم قرار داده‌ام که به صورت خودکار و یکی پس از دیگری اجرا و نصب خواهند شد.
پس از آن با کلیک بر روی فایلی با پسوند vsto که در پوشه build برنامه موجود است، می‌توان افزونه را نصب کرد (click once installation).




سایر اطلاعات در مورد پروژه‌های VSTO را می‌توان از طریق وبلاگ رسمی آنها دنبال کرد:
http://blogs.msdn.com/vsto/

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


مطالب
معرفی Lex.Db
Lex.Db یک بانک اطلاعاتی درون پروسه‌ای (مدفون شده یا embedded) بسیار سریع نوشته شده با سی‌شارپ است. این بانک اطلاعاتی کم حجم، سورس باز بوده و مجوز استفاده از آن LGPL است. به این معنا که استفاده از اسمبلی‌های آن در هر نوع پروژه‌ای آزاد است.
نکته مهم آن سازگاری با برنامه‌های دات نت 4 به بعد، همچنین برنامه‌های ویندوز 8، سیلورلایت 5، ویندوز فون 8 و همچنین اندروید (از طریق Mono) است. به علاوه چون با دات نت تهیه شده است، دیگر نیازی نیست دو نگارش 32 بیتی و 64 بیتی آن توزیع شوند و به این ترتیب مشکلات توزیع بانک‌های اطلاعاتی native مانند SQLite را ندارد ( و مطابق ادعای نویسنده آلمانی آن، از SQLite سریعتر است).
API این بانک اطلاعاتی، هر دو نوع متدهای synchronous  و  asynchronous را شامل می‌شود؛ به همین جهت با برنامه‌های ویندوز 8 و سیلورلایت نیز سازگاری دارد.
Lex.Db از برنامه‌های چندریسمانی و همچنین استفاده از یک بانک اطلاعاتی آن توسط چندین پروسه همزمان نیز پشتیبانی می‌کند.
در ادامه مروری خواهیم داشت بر نحوه استفاده از آن در حالت طراحی رابطه‌ای؛ از این جهت که فعلا به ظاهر این بانک اطلاعاتی روابط را پشتیبانی نمی‌کند، اما در عمل پیاده سازی آن مشکل نیست.

دریافت Lex.Db

برای دریافت Lex.Db، دستور ذیل را در خط فرمان پاورشل نیوگت وارد نمائید:
 PM> Install-Package Lex.Db
بسته به نوع پروژه شما (دات نت یا WinRT یا ...)، اسمبلی متناسبی به پروژه اضافه خواهد شد.


مدل‌های برنامه

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
    }

    public class Order
    {
        public int Id { get; set; }
        public int? CustomerFK { get; set; }
        public int[] ProductsFK { get; set; }
    }
مدل‌های برنامه آزمایشی مطلب جاری را در اینجا ملاحظه می‌کنید. برای طراحی روابط یک به صفر یا یک و همچنین یک به چند، تنها کافی است کلیدهای اصلی یا آرایه‌ای از کلیدهای اصلی مرتبط را در اینجا ذخیره کنیم، که نمونه‌ای از آن‌را در کلاس Order ملاحظه می‌کنید.


آغاز بانک اطلاعاتی

    public static class Database
    {
        public static DbInstance Instance { get; private set; }

        public static DbTable<Product> Products { get; private set; }
        public static DbTable<Order> Orders { get; private set; }
        public static DbTable<Customer> Customers { get; private set; }

        /// <summary>
        /// سازنده استاتیکی که در طول عمر برنامه فقط یکبار اجرا می‌شود
        /// </summary>
        static Database()
        {
            createDb();
            getTables();
        }

        private static void getTables()
        {
            Products = Instance.Table<Product>();
            Customers = Instance.Table<Customer>();
            Orders = Instance.Table<Order>();
        }

        private static void createDb()
        {
            Instance = new DbInstance(Path.Combine(Environment.CurrentDirectory, "LexDbTests"));

            Instance.Map<Product>()
                    .WithIndex("NameIdx", x => x.Name)
                    .Automap(i => i.Id, true);

            Instance.Map<Order>()
                    .Automap(i => i.Id, true);

            Instance.Map<Customer>()
                    .WithIndex("NameIdx", x => x.Name)
                    .WithIndex("CityIdx", x => x.City)
                    .Automap(i => i.Id, true);

            Instance.Initialize();
        }
    }
کلاس دیتابیس و سازنده آن، استاتیک تعریف شده‌اند؛ تا در طول عمر برنامه تنها یکبار وهله سازی شوند. new DbInstance یک وهله جدید از بانک اطلاعاتی را آغاز می‌کند. سازنده آن، مسیر پوشه‌ای که فایل‌های این بانک اطلاعاتی در آن ذخیره خواهند شد را دریافت می‌کند. Lex.Db به ازای هر کلاس مدلی که به آن معرفی شود، دو فایل data و index را ایجاد می‌کند.
سپس توسط وهله‌ای از بانک اطلاعاتی که ایجاد کردیم، کار معرفی خواص مدل‌های برنامه توسط متد Map و Automap انجام می‌شود. متد Automap خاصیت primary key کلاس را دریافت کرده و همچنین پارامتر دوم آن مشخص می‌کند که آیا این کلید اصلی به صورت خودکار ایجاد شود یا خیر. به علاوه در همینجا می‌توان روی فیلدهای مختلف، ایندکس نیز ایجاد کرد. متد WithIndex یک نام دلخواه را دریافت کرده و سپس خاصیتی را که باید بر روی آن ایندکس ایجاد شود، دریافت می‌کند.
در نهایت متد Initialize باید فراخوانی گردد. البته اگر برنامه شما WinRT است، این متد Initialize Async خواهد بود.
جداول نیز بر اساس مدل‌های برنامه از طریق متد Instance.Table در دسترس قرار گرفته‌اند.

افزودن اطلاعات به بانک اطلاعاتی
        private static void addData()
        {
            var customer1 = new Customer { Name = "customer1", City = "City1" };
            var customer2 = new Customer { Name = "customer2", City = "City2" };
            Database.Instance.Save(customer1, customer2); // automatic Id assignment after Save

            var product1 = new Product { Name = "product1" };
            var product2 = new Product { Name = "product2" };
            Database.Instance.Save(product1, product2); // automatic Id assignment after Save

            var order1 = new Order { CustomerFK = customer1.Id, ProductsFK = new[] { product1.Id } };
            var order2 = new Order { CustomerFK = customer2.Id, ProductsFK = new[] { product1.Id, product2.Id } };
            Database.Instance.Save(order1, order2); // automatic Id assignment after Save
        }
اکنون که کار آغاز بانک اطلاعاتی صورت گرفت، برای افزودن اطلاعات از متد Database.Instance.Save می‌توان استفاده کرد (در برنامه‌های WinRT از  متد Save Async استفاده کنید).
در اینجا نیازی به ذکر Id نمونه‌های ساخته شده نیست؛ از این جهت که در حین عملیات Save، به صورت خودکار انتساب خواهند یافت.
همچنین نحوه مقدار دهی کلیدهای خارجی نیز با استفاده از همین کلیدهای اصلی آماده شده است.


واکشی تمام اطلاعات

        private static void loadAll()
        {
            var orders = Database.Orders.LoadAll();
            foreach (var order in orders)
            {
                // نحوه دریافت اطلاعات مشتری بر اساس کلید خارجی ثبت شده
                var orderCustomer = Database.Customers.LoadByKey(order.CustomerFK.Value);
                Console.WriteLine("Order Id: {0}, Customer: {1} ({2}) {3}", order.Id, orderCustomer.Name, orderCustomer.Id, orderCustomer.City);

                // نحوه بازیابی لیستی از اشیاء مرتبط از طریق آرایه‌ای از کلیدهای خارجی ثبت شده
                var orderProducts = Database.Products.LoadByKeys(order.ProductsFK);
                foreach (var product in orderProducts)
                {
                    Console.WriteLine("  Product Id: {0}, Name: {1}", product.Id, product.Name);
                }
            }
        }
بانک اطلاعاتی آغاز شد؛ تعدادی رکورد نیز در آن ثبت گردید. اکنون برای بازیابی اطلاعات می‌توان از متدهای در دسترس جداول کلاس Database استفاده کرد. برای مثال متد LoadAll تمام رکوردهای یک جدول را واکشی می‌کند (در برنامه‌های WinRT این متد LoadAll Async خواهد بود).
سپس با استفاده از متدهای LoadByKey و LoadByKeys، به سادگی می‌توان اشیاء مرتبط با هر سفارش را نیز واکشی کرد.


استفاده از ایندکس‌ها برای کوئری گرفتن

        private static void queryingByAnIndex()
        {
            var name = "customer1";
            var customersList = Database.Customers
                                        .IndexQueryByKey("NameIdx", name)
                                        .ToList();
            foreach (var person in customersList)
            {
                Console.WriteLine(person.Name);
            }
        }
در ابتدای بحث، توسط متد WithIndex، تعدادی ایندکس را نیز تعریف کردیم. اکنون توسط این ایندکس‌ها و متد IndexQueryByKey، می‌توان کوئری‌هایی بسیار سریع را تهیه کرد.
            // Using Take and Skip
            var list1 = Database.Orders.Query<int>() // primary idx
                                       .Take(1).Skip(2).ToList();

            // Querying Between Ranges 
            var list2 = Database.Customers
                                .IndexQuery<string>("NameIdx")
                                .GreaterThan("a", orEqual: true).LessThan("d").ToList();
همچنین در اینجا متدهایی مانند Take و Skip و یا جستجو در یک بازه توسط متدهای GreaterThan و LessThan نیز پشتیبانی می‌شوند.


حذف رکوردها
        private static void deletingRecords()
        {
            Database.Customers.DeleteByKey(key: 1);

            var customers = Database.Customers.LoadByKeys(new[] { 1, 2 });
            Database.Customers.Delete(customers);
        }
برای حذف رکوردها از متدهای DeleteByKey و یا Delete می‌توان استفاده کرد. متد Delete می‌تواند آرایه‌ای از اشیاء را نیز قبول کند.
و اگر خواستید کل بانک اطلاعاتی را خالی کنید، متد Database.Instance.Purge اینکار را انجام خواهد داد.


کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید:
Program-LexDb.cs
 
نظرات مطالب
چگونه از SVN جهت به روز رسانی یک سایت استفاده کنیم؟
بحث اینجا کار تیمی است نه تک نفره و deploy به سرور. به همین جهت صحبت از مخزن کد شد و SVN.
این‌ها مشکلات web deploy است:
-با IIS 6 آنچنان سازگار نیست و IIS7 را طلب می‌کند. (با روش فوق سرور شما آپاچی هم باشد کار می‌کند)
-بحث rollback با webdelpoy اصلا معنی ندارد. اما با سورس کنترل به سادگی انجام می‌شود. فرض کنید الان به اشتباه یک سری کار به مخزن SVN ارسال شده. بلافاصله هم با روش فوق در ریشه سایت قرار گرفته. اصطلاحا revert به نگارش پایدار قبلی در SVN بسیار ساده است.
+ تمام مزیت‌های ورژن کنترل را هم لحاظ کنید. با روش فوق دقیقا مشخص است چه کسانی روی فایل‌ها کار کرده‌اند و چه زمانی. تاریخچه ارسال‌ها موجود است. امکان حرکت بین نگارش‌های مختلف و سوئیچ بین آن‌ها معنا پیدا می‌کند و ...
- روش فوق نیاز آنچنانی به داشتن دسترسی بالا روی سرور ندارد.

به همین جهت برای کار حرفه‌ای با web deploy از برنامه team city و یک سری مخلفات دیگر استفاده می‌کنند. یک سری 5 قسمتی رو اینجا می‌تونید پیدا کنید: (+)
و پس از مطالعه به این نتیجه خواهید رسید که روش فوق پایدارتر و دردسر کمتری دارد. حتی روی یک سرور لینوکسی هم قابل پیاده سازی است
مطالب
آشنایی با NHibernate - قسمت هفتم

مدیریت بهینه‌ی سشن فکتوری

ساخت یک شیء SessionFactory بسیار پر هزینه و زمانبر است. به همین جهت لازم است که این شیء یکبار حین آغاز برنامه ایجاد شده و سپس در پایان کار برنامه تخریب شود. انجام اینکار در برنامه‌های معمولی ویندوزی (WinForms ،WPF و ...)، ساده است اما در محیط Stateless وب و برنامه‌های ASP.Net ، نیاز به راه حلی ویژه وجود خواهد داشت و تمرکز اصلی این مقاله حول مدیریت صحیح سشن فکتوری در برنامه‌های ASP.Net است.

برای پیاده سازی شیء سشن فکتوری به صورتی که یکبار در طول برنامه ایجاد شود و بارها مورد استفاده قرار گیرد باید از یکی از الگوهای معروف طراحی برنامه نویسی شیء گرا به نام Singleton Pattern استفاده کرد. پیاده سازی نمونه‌ی thread safe آن که در برنامه‌های ذاتا چند ریسمانی وب و همچنین برنامه‌های معمولی ویندوزی می‌تواند مورد استفاده قرار گیرد، در آدرس ذیل قابل مشاهده است:



از پنجمین روش ذکر شده در این مقاله جهت ایجاد یک lazy, lock-free, thread-safe singleton استفاده خواهیم کرد.

بررسی مدل برنامه

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


یک برنامه ASP.Net را آغاز کرده و ارجاعاتی را به اسمبلی‌های زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و همچنین ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

تصویر نهایی پروژه ما به شکل زیر خواهد بود:



پروژه ما دارای یک پوشه domain ، تعریف کننده موجودیت‌های برنامه جهت تهیه نگاشت‌های لازم از روی ‌آن‌ها است. سپس یک پوشه جدید را به نام NHSessionManager به آن جهت ایجاد یک Http module مدیریت کننده سشن‌های NHibernate در برنامه اضافه خواهیم کرد.

ساختار دومین برنامه (مطابق کلاس دیاگرام فوق):

namespace NHSample3.Domain
{
public class Car
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Color { get; set; }
}
}

using System.Collections.Generic;

namespace NHSample3.Domain
{
public class Parking
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Location { get; set; }
public virtual IList<Car> Cars { get; set; }

public Parking()
{
Cars = new List<Car>();
}
}
}
مدیریت سشن فکتوری در برنامه‌های وب

در این قسمت قصد داریم Http Module ایی را جهت مدیریت سشن‌های NHibernate ایجاد نمائیم.

در ابتدا کلاس Config را در پوشه مدیریت سشن NHibernate با محتویات زیر ایجاد کنید:

using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;

namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.ExposeConfiguration(
x => x.SetProperty("current_session_context_class", "managed_web")
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample3.Domain.Car).Assembly))
);
}

public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
با این کلاس در قسمت‌های قبل آشنا شده‌اید. در این کلاس با کمک امکانات Auto mapping موجود در Fluent Nhibernate (مطلب قسمت قبلی این سری آموزشی) اقدام به تهیه نگاشت‌های خودکار از کلاس‌های قرار گرفته در پوشه دومین خود خواهیم کرد (فضای نام این پوشه به دومین ختم می‌شود که در متد GetConfig مشخص است).
دو نکته جدید در متد GetConfig وجود دارد:
الف) استفاده از متد FromConnectionStringWithKey ، بجای تعریف مستقیم کانکشن استرینگ در متد مذکور که روشی است توصیه شده. به این صورت فایل وب کانفیگ ما باید دارای تعریف کلید مشخص شده در متد GetConfig به نام DbConnectionString باشد:

<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true" />
</connectionStrings>
ب) قسمت ExposeConfiguration آن نیز جدید است.
در اینجا به AutoMapper خواهیم گفت که قصد داریم از امکانات مدیریت سشن مخصوص وب فریم ورک NHibernate استفاده کنیم. فریم ورک NHibernate دارای کلاسی است به نام NHibernate.Context.ManagedWebSessionContext که جهت مدیریت سشن‌های خود در پروژه‌های وب ASP.Net پیش بینی کرده است و از این متد در Http module ایی که ایجاد خواهیم کرد جهت ردگیری سشن جاری آن کمک خواهیم گرفت.

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



سپس کلاس SingletonCore را جهت تهیه تنها و تنها یک وهله از شیء سشن فکتوری در کل برنامه ایجاد خواهیم کرد (همانطور که عنوان شده، ایده پیاده سازی این کلاس thread safe ، از مقاله معرفی شده در ابتدای بحث گرفته شده است):

using NHibernate;

namespace NHSessionManager
{
/// <summary>
/// lazy, lock-free, thread-safe singleton
/// </summary>
public class SingletonCore
{
private readonly ISessionFactory _sessionFactory;

SingletonCore()
{
_sessionFactory = Config.GetConfig().BuildSessionFactory();
}

public static SingletonCore Instance
{
get
{
return Nested.instance;
}
}

public static ISession GetCurrentSession()
{
return Instance._sessionFactory.GetCurrentSession();
}

public static ISessionFactory SessionFactory
{
get { return Instance._sessionFactory; }
}

class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}

internal static readonly SingletonCore instance = new SingletonCore();
}
}
}
اکنون می‌توان از این Singleton object جهت تهیه یک Http Module کمک گرفت. برای این منظور کلاس SessionModule را به برنامه اضافه کنید:

using System;
using System.Web;
using NHibernate;
using NHibernate.Context;

namespace NHSessionManager
{
public class SessionModule : IHttpModule
{
public void Dispose()
{ }

public void Init(HttpApplication context)
{
if (context == null)
throw new ArgumentNullException("context");

context.BeginRequest += Application_BeginRequest;
context.EndRequest += Application_EndRequest;
}

private void Application_BeginRequest(object sender, EventArgs e)
{
ISession session = SingletonCore.SessionFactory.OpenSession();
ManagedWebSessionContext.Bind(HttpContext.Current, session);
session.BeginTransaction();
}

private void Application_EndRequest(object sender, EventArgs e)
{
ISession session = ManagedWebSessionContext.Unbind(
HttpContext.Current, SingletonCore.SessionFactory);
if (session == null) return;

try
{
if (session.Transaction != null &&
!session.Transaction.WasCommitted &&
!session.Transaction.WasRolledBack)
{
session.Transaction.Commit();
}
else
{
session.Flush();
}
}
catch (Exception)
{
session.Transaction.Rollback();
}
finally
{
if (session != null && session.IsOpen)
{
session.Close();
session.Dispose();
}
}
}
}
}
کلاس فوق کار پیاده سازی اینترفیس IHttpModule را جهت دخالت صریح در request handling pipeline برنامه ASP.Net جاری انجام می‌دهد. در این کلاس مدیریت متدهای استاندارد Application_BeginRequest و Application_EndRequest به صورت خودکار صورت می‌گیرد.
در متد Application_BeginRequest ، در ابتدای هر درخواست یک سشن جدید ایجاد و به مدیریت سشن وب NHibernate بایند می‌شود، همچنین یک تراکنش نیز آغاز می‌گردد. سپس در پایان درخواست، این انقیاد فسخ شده و تراکنش کامل می‌شود، همچنین کار پاکسازی اشیاء نیز صورت خواهد گرفت.

با توجه به این موارد، دیگر نیازی به ذکر using جهت dispose کردن سشن جاری در کدهای ما نخواهد بود، زیرا در پایان هر درخواست اینکار به صورت خودکار صورت می‌گیرد. همچنین نیازی به ذکر تراکنش نیز نمی‌باشد، چون مدیریت آن‌را خودکار کرده‌ایم.

جهت استفاده از این Http module تهیه شده باید چند سطر زیر را به وب کانفیگ برنامه اضافه کرد:

<httpModules>
<!--NHSessionManager-->
<add name="SessionModule" type="NHSessionManager.SessionModule"/>
</httpModules>
بدیهی است اگر نخواهید از Http module استفاده کنید باید این کدها را در فایل Global.asax برنامه قرار دهید.

اکنون مثالی از نحوه‌ی استفاده از امکانات فراهم شده فوق به صورت زیر می‌تواند باشد:
ابتدا کلاس ParkingContext را جهت مدیریت مطلوب‌تر LINQ to NHibernate تشکیل می‌دهیم.

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample3.Domain;

namespace NHSample3
{
public class ParkingContext : NHibernateContext
{
public ParkingContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<Car> Cars
{
get { return Session.Linq<Car>(); }
}

public IOrderedQueryable<Parking> Parkings
{
get { return Session.Linq<Parking>(); }
}
}
}
سپس در فایل Default.aspx.cs برنامه ، برای نمونه تعدادی رکورد را افزوده و نتیجه را در یک گرید ویوو نمایش خواهیم داد:

using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample3.Domain;
using NHSessionManager;

namespace NHSample3
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//ایجاد دیتابیس در صورت نیاز
//Config.CreateDb();

//ثبت یک سری رکورد در دیتابیس
ISession session = SingletonCore.GetCurrentSession();

Car car1 = new Car() { Name = "رنو", Color = "مشکلی" };
session.Save(car1);
Car car2 = new Car() { Name = "پژو", Color = "سفید" };
session.Save(car2);

Parking parking1 = new Parking()
{
Location = "آدرس پارکینگ مورد نظر",
Name = "پارکینگ یک",
Cars = new List<Car> { car1, car2 }
};

session.Save(parking1);

//نمایش حاصل در یک گرید ویوو
ParkingContext db = new ParkingContext(session);
var query = from x in db.Cars select new { CarName = x.Name, CarColor = x.Color };
GridView1.DataSource = query.ToList();
GridView1.DataBind();
}
}
}
مدیریت سشن فکتوری در برنامه‌های غیر وب

در برنامه‌های ویندوزی مانند WinForms ، WPF و غیره، تا زمانیکه یک فرم باز باشد، کل فرم و اشیاء مرتبط با آن به یکباره تخریب نخواهند شد، اما در یک برنامه ASP.Net جهت حفظ منابع سرور در یک محیط چند کاربره، پس از پایان نمایش یک صفحه وب، اثری از آثار اشیاء تعریف شده در کدهای آن صفحه در سرور وجود نداشته و همگی بلافاصله تخریب می‌شوند. به همین جهت بحث‌های ویژه state management در ASP.Net در اینباره مطرح است و مدیریت ویژه‌ای باید روی آن صورت گیرد که در قسمت قبل مطرح شد.
از بحث فوق، تنها استفاده از کلاس‌های Config و SingletonCore ، جهت استفاده و مدیریت بهینه‌ی سشن فکتوری در برنامه‌های ویندوزی کفایت می‌کنند.

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

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

مطالب
آموزش Prism #2
در پست قبلی توضیح کلی درباره فریم ورک Prism داده شد. در این بخش قصد داریم آموزش‌های داده شده در پست قبلی را با هم در یک مثال مشاهده کنیم. در پروژه‌های ماژولار طراحی و ایجاد زیر ساخت قوی برای مدیریت ماژول‌ها بسیار مهم است. Prism فریم ورکی است که فقط چارچوب و قواعد اصول طراحی این گونه پروژه‌ها را در اختیار ما قرار می‌دهد. در پروژه‌های ماژولار هر ماژول باید در یک اسمبلی جدا قرار داشته باشد که ساختار پیاده سازی آن می‌تواند کاملا متفاوت با پیاده سازی سایر ماژول‌ها باشد.
 برای شروع  باید فایل‌های اسمبلی Prism رو دانلود کنید(لینک دانلود).
تشریح پروژه:
می‌خواهیم برنامه ای بنویسیم که دارای سه ماژول زیر است.:
  1. ماژول Navigator : برای انتخاب و Switch کردن بین ماژول‌ها استفاده می‌شود؛
  2. ماژول طبقه بندی کتاب‌ها : لیست طبقه بندی کتاب‌ها را به ما نمایش می‌دهد؛
  3. ماژول لیست کتاب‌ها : عناوین کتاب‌ها به همراه نویسنده و کد کتاب را به ما نمایش می‌دهد.

*در این پروژه از UnityContainer برای مباحث Dependency Injection استفاده شده است.
ابتدا یک پروژه WPF در Vs.Net ایجاد کنید(در اینجا من نام آن را  FirstPrismSample گذاشتم). قصد داریم یک صفحه طراحی کنیم که دو ماژول مختلف در آن لود شود. ابتدا باید Shell پروژه رو طراحی کنیم. یک Window جدید به نام Shell بسازید و کد زیر را در آن کپی کنید.
<Window x:Class="FirstPrismSample.Shell"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:com="http://www.codeplex.com/CompositeWPF"
    Title="Prism Sample By Masoud Pakdel" Height="400" Width="600" WindowStartupLocation="CenterScreen">
    <DockPanel>
      <ContentControl com:RegionManager.RegionName="WorkspaceRegion" Width="400"/>
      <ContentControl com:RegionManager.RegionName="NavigatorRegion"  DockPanel.Dock="Left" Width="200" />     
    </DockPanel>
</Window>
در این صفحه دو ContentControl تعریف کردم یکی به نام Navigator و دیگری به نام Workspace. به وسیله RegionName که یک AttachedProperty است هر کدوم از این نواحی را برای Prism تعریف کردیم. حال باید یک ماژول برای Navigator و دو ماژول دیگر یکی برای طبقه بندی کتاب‌ها و دیگری برای لیست کتاب‌ها بسازیم.

#پروژه Common
قبل از هر چیز یک پروژه Common می‌سازیم و مشترکات بین ماژول‌ها رو در آن قرار می‌دهیم(این پروژه باید به تمام ماژول‌ها رفرنس داده شود).  این مشترکات شامل :
  • کلاس پایه ViewModel
  • کلاس ViewRequestEvent
  • کلاس ModuleService

کد کلاس ViewModelBase که فقط اینترفیس INotifyPropertyChanged رو پیاده سازی کرده است:

using System.ComponentModel;

namespace FirstPrismSample.Common
{
    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChangedEvent( string propertyName )
        {
            if ( PropertyChanged != null )
            {
                PropertyChangedEventArgs e = new PropertyChangedEventArgs( propertyName );
                PropertyChanged( this, e );
            }
        }
    }
}
کلاس ViewRequestEvent که به صورت زیر است:
using Microsoft.Practices.Composite.Presentation.Events;

namespace FirstPrismSample.Common.Events
{
    public class ViewRequestedEvent : CompositePresentationEvent<string>
    {
    }
}
توضیح درباره CompositePresentationEvent :
در طراحی و توسعه پروژه‌های ماژولار نکته ای که باید به آن دقت کنید این است که ماژول‌های پروژه نباید به هم وابستگی مستقیم داشته باشند در عین حال ماژول‌ها باید بتوانند با هم در ارتباط باشند. CPE یا CompositePresentationEventدقیقا برای این منظور به وجود آمده است. CPE که در این جا طراحی کردم فقط کلاسی است که از CompositePresentationEventارث برده است و دلیل آن که به صورت string generic استفاده شده است این است که می‌خواهیم در هر درخواست نام ماژول درخواستی را داشته باشیم و به همین دلیل نام آن را ViewRequestedEvent گذاشتم.

توضیح درباره EventAggregator

EventAggregator یا به اختصار EA مکانیزمی است در پروژهای ماژولار برای اینکه در Composite UI‌ها بتوانیم بین کامپوننت‌ها ارتباط برقرار کنیم. استفاده از EA وابستگی بین ماژول‌ها را  از بین خواهد برد. برنامه نویسانی که با MVVM Light آشنایی دارند از قابلیت Messaging موجود در این فریم ورک برای ارتباط بین View و  ViewModel استفاده می‌کنند. در Prism این عملیات توسط EA انجام می‌شود. یعنی برای ارتباط با View‌ها باید از EA تعبیه شده در Prism استفاده کنیم. در ادامه مطلب، چگونگی استفاده از EA را خواهید آموخت.
اینترفیس IModuleService که فقط شامل یک متد است:
namespace FirstPrismSample .Common
{
    public interface IModuleServices
    {     
        void ActivateView(string viewName);
    }
}
کلاس ModuleService که اینترفیس بالا را پیاده سازی کرده است:
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;

namespace FirstPrismSample.Common
{
    public class ModuleServices : IModuleServices
    {     
        private readonly IUnityContainer m_Container;  
     
        public ModuleServices(IUnityContainer container)
        {
            m_Container = container;
        }      
   
        public void ActivateView(string viewName)
        {        
            var regionManager = m_Container.Resolve<IRegionManager>();

            // غیر فعال کردن ویو
            IRegion workspaceRegion = regionManager.Regions["WorkspaceRegion"];
            var views = workspaceRegion.Views;
            foreach (var view in views)
            {
                workspaceRegion.Deactivate(view);
            }

            //فعال کردن ویو انتخاب شده 
            var viewToActivate = regionManager.Regions["WorkspaceRegion"].GetView(viewName);
            regionManager.Regions["WorkspaceRegion"].Activate(viewToActivate);
        }
    }
}
متد ActivateView نام view مورد نظر برای فعال سازی را دریافت می‌کند. برای فعال کردن View ابتدا باید سایر view‌های فعال در RegionManager را غیر فعال کنیم. سپس فقط view مورد نظر در RegionManager انتخاب و فعال می‌شود.

*نکته: در هر ماژول ارجاع به اسمبلی‌های Prism مورد نیاز است.

#ماژول طبقه بندی کتاب ها:
برای شروع یک Class Library جدید به نام ModuleCategory به پروژه اضافه کنید. یک UserControl به نام CategoryView بسازید و کد‌های زیر را در آن کپی کنید.
<UserControl x:Class="FirstPrismSample.ModuleCategory.CategoryView "
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             Background="LightGray" FlowDirection="RightToLeft" FontFamily="Tahoma">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Text=" طبقه بندی ها"/>
        <ListView Grid.Row="1"  Margin="10" Name="lvCategory">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="کد" Width="50" />
                    <GridViewColumn Header="عنوان" Width="200"  />                  
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</UserControl>
یک کلاس به نام CategoryModule بسازید که اینترفیس IModule رو پیاده سازی کند.
using Microsoft.Practices.Composite.Events;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
using FirstPrismSample.Common;
using FirstPrismSample.Common.Events;
using Microsoft.Practices.Composite.Presentation.Events;

namespace FirstPrismSample.ModuleCategory
{
    [Module(ModuleName = "ModuleCategory")]
    public class CategoryModule : IModule
    {      
        private readonly IUnityContainer m_Container;
        private readonly string moduleName = "ModuleCategory";
            
        public CategoryModule(IUnityContainer container)
        {
            m_Container = container;
        }   
      
        ~CategoryModule()
        {
            var eventAggregator = m_Container.Resolve<IEventAggregator>();
            var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();       
            viewRequestedEvent.Unsubscribe(ViewRequestedEventHandler);
        }
     
        public void Initialize()
        {           
            var regionManager = m_Container.Resolve<IRegionManager>();
            regionManager.Regions["WorkspaceRegion"].Add(new CategoryView(), moduleName);
         
            var eventAggregator = m_Container.Resolve<IEventAggregator>();
            var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
            viewRequestedEvent.Subscribe(this.ViewRequestedEventHandler, true);
        }
       
        public void ViewRequestedEventHandler(string moduleName)
        {
            if (this.moduleName != moduleName) return;
          
            var moduleServices = m_Container.Resolve<IModuleServices>();
            moduleServices.ActivateView(moduleName);
        }      
    }
}
چند نکته :
*ModuleAttribute استفاده شده در بالای کلاس برای تعیین نام ماژول استفاده می‌شود. این Attribute دارای دو خاصیت دیگر هم است :
  1. OnDemand : برای تعیین اینکه ماژول باید به صورت OnDemand (بنا به درخواست) لود شود.
  2. StartupLoaded : برای تعیین اینکه ماژول به عنوان ماژول اول پروزه لود شود.(البته این گزینه Obsolute شده است)

*برای تعریف ماژول کلاس مورد نظر حتما باید اینترفیس IModule را پیاده سازی کند. این اینترفیس فقط شامل یک متد است به نام Initialize.

*در این پروژه چون View‌های برنامه صرفا جهت نمایش هستند در نتیجه نیاز به ایجاد ViewModel برای آن‌ها نیست. در پروژه‌های اجرایی حتما برای هر View باید ViewModel متناظر با آن تهیه شود.

توضیح درباره متد Initialize

در این متد ابتدا با استفاده از Container موجود RegionManager را به دست می‌آوریم. با استفاده از RegionManager می‌تونیم یک CompositeUI طراحی کنیم. در فایل Shell مشاهده کردید که یک صفحه به دو ناحیه تقسیم شد و به هر ناحیه هم یک نام اختصاص دادیم. دستور زیر به یک ناحیه اشاره خواهد داشت:

regionManager.Regions["WorkspaceRegion"]
در خط بعد با استفاده از EA یا Event Aggregator توانستیم CPE را بدست بیاوریم. متد Subscribe در کلاس CPE  یک ارجاع قوی به delegate مورد نظر ایجاد می‌کند(پارامتر دوم این متد که از نوع boolean است) که به این معنی است که این delegate هیچ گاه توسط GC جمع آوری نخواهد شد. در نتیجه، قبل از اینکه ماژول بسته شود باید به صورت دستی این کار را انجام دهیم که مخرب را برای همین ایجاد کردیم. اگر به کد‌های مخرب دقت کنید می‌بینید که با استفاده از EA توانستیم ViewRequestEventHandler را Unsubscribe کنیم به دلیل اینکه از ارجاع قوی با strong Reference در متد Subscribe استفاده شده است.
دستور moduleService.ActiveateView ماژول مورد نظر را در region مورد نظر هاست خواهد کرد.

#ماژول لیست کتاب ها:
ابتدا یک Class Library به نام ModuleBook بسازید  و همانند ماژول قبلی نیاز به یک Window و یک کلاس داریم:
BookWindow که کاملا مشابه به CategoryView است.
<UserControl x:Class="FirstPrismSample.ModuleBook.BookView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Background="LightGray" FontFamily="Tahoma" FlowDirection="RightToLeft">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Text="لیست کتاب ها"/>
        <ListView Grid.Row="1" Margin="10" Name="lvBook">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="کد" Width="50"  />
                    <GridViewColumn Header="عنوان" Width="200" />
                    <GridViewColumn Header="نویسنده" Width="150" />
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</UserControl>

کلاس BookModule که پیاده سازی  و توضیحات آن کاملا مشابه به CategoryModule می‌باشد.
using Microsoft.Practices.Composite.Events;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.Presentation.Events;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
using FirstPrismSample.Common;
using FirstPrismSample.Common.Events;

namespace FirstPrismSample.ModuleBook
{
    [Module(ModuleName = "moduleBook")]
    public class BookModule : IModule
    {      
        private readonly IUnityContainer m_Container;
        private readonly string moduleName = "ModuleBook";     
    
        public BookModule(IUnityContainer container)
        {
            m_Container = container;          
        }     
       
        ~BookModule()
        {           
            var eventAggregator = m_Container.Resolve<IEventAggregator>();
            var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
          
            viewRequestedEvent.Unsubscribe(ViewRequestedEventHandler);
        }     
     
        public void Initialize()
        {           
            var regionManager = m_Container.Resolve<IRegionManager>();
            var view = new BookView();
            regionManager.Regions["WorkspaceRegion"].Add(view, moduleName);
            regionManager.Regions["WorkspaceRegion"].Deactivate(view);
      
            var eventAggregator = m_Container.Resolve<IEventAggregator>();
            var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
            viewRequestedEvent.Subscribe(this.ViewRequestedEventHandler, true);
        }     
      
        public void ViewRequestedEventHandler(string moduleName)
        {           
            if (this.moduleName != moduleName) return;
         
            var moduleServices = m_Container.Resolve<IModuleServices>();
            moduleServices.ActivateView(m_WorkspaceBName);
        }
    }
}
#ماژول Navigator
برای این ماژول هم ابتدا View مورد نظر را ایجاد می‌کنیم:
<UserControl x:Class="FirstPrismSample.ModuleNavigator.NavigatorView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <Grid>
        <StackPanel VerticalAlignment="Center">
            <TextBlock Text="انتخاب ماژول" Foreground="Green" HorizontalAlignment="Center"
                VerticalAlignment="Center" FontFamily="Tahoma" FontSize="24" FontWeight="Bold" />
        <Button Command="{Binding ShowModuleCategory}" Margin="5" Width="125">طبقه بندی کتاب ها</Button>
        <Button Command="{Binding ShowModuleBook}" Margin="5" Width="125">لیست کتاب ها</Button>
        </StackPanel>
        </Grid>
</UserControl>
حال قصد داریم برای این View یک ViewModel بسازیم. نام آن را INavigatorViewModel خواهیم گذاشت:
public interface INavigatorViewModel
    {    
        ICommand ShowModuleCategory { get; set; }       
        ICommand ShowModuleBook { get; set; }

        string ActiveWorkspace { get; set; }       

        IUnityContainer Container { get; set; }

        event PropertyChangedEventHandler PropertyChanged;
    }
 *در اینترفیس بالا دو Command داریم که هر کدام وظیفه لود یک ماژول را بر عهده دارند.
 *خاصیت ActiveWorkspace برای تعیین workspace فعال تعریف شده است.

حال به پیاده سازی مثال بالا می‌پردازیم:
public class NavigatorViewModel : ViewModelBase, INavigatorViewModel
    {        
        public NavigatorViewModel(IUnityContainer container)
        {
            this.Initialize(container);
        }   
       
        public ICommand ShowModuleCategory { get; set; }
      
        public ICommand ShowModuleBook { get; set; }      
              
        public string ActiveWorkspace { get; set; }       

        public IUnityContainer Container { get; set; }        
     
        private void Initialize(IUnityContainer container)
        {
            this.Container = container;
            this.ShowModuleCategory = new ShowModuleCategoryCommand(this);
            this.ShowModuleBook = new ShowModuleBookCommand(this);
            this.ActiveWorkspace = "ModuleCategory";
        }        
    }
تنها نکته مهم در کلاس بالا متد Initialize است که دو Command مورد نظر را پیاده سازی کرده است. ماژول پیش فرض هم ماژول طبقه بندی کتاب‌ها یا ModuleCategory در نظر گرفته شده است.  همان طور که می‌بینید پیاده سازی Command‌ها بالا توسط دو کلاس ShowModuleCategoryCommand و ShowModuleBookCommand انجام شده که در زیر کد‌های آن‌ها را می‌بینید.
#کد کلاس ShowModuleCategoryCommand  
public class ShowModuleCategoryCommand : ICommand
    {      
        private readonly NavigatorViewModel viewModel;
        private const string workspaceName = "ModuleCategory";         

        public ShowModuleCategoryCommand(NavigatorViewModel viewModel)
        {
            this.viewModel = viewModel;
        }          

        public bool CanExecute(object parameter)
        {
            return viewModel.ActiveWorkspace != workspaceName;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
     
        public void Execute(object parameter)
        {
            CommandServices.ShowWorkspace(workspaceName, viewModel);
        }      
    }
#کد کلاس ShowModuleBookCommand  
public class ShowModuleBookCommand : ICommand
    {
        private readonly NavigatorViewModel viewModel;
        private readonly string workspaceName = "ModuleBook";

        public ShowModuleBookCommand( NavigatorViewModel viewModel )
        {
            this.viewModel = viewModel;
        }

        public bool CanExecute( object parameter )
        {
            return viewModel.ActiveWorkspace != workspaceName;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute( object parameter )
        {
            CommandServices.ShowWorkspace( workspaceName , viewModel );
        }
    }
با توجه به این که فرض است با متد‌های Execute و CanExecute و CanExecuteChanged آشنایی دارید از توضیح این مطالب خودداری خواهم کرد. فقط کلاس CommandServices  در متد Execute دارای متدی به نام ShowWorkspace است که کد‌های زیر را شامل می‌شود:
public static void ShowWorkspace(string workspaceName, INavigatorViewModel viewModel)
  {           
            var eventAggregator = viewModel.Container.Resolve<IEventAggregator>();
            var viewRequestedEvent = eventAggregator.GetEvent<ViewRequestedEvent>();
            viewRequestedEvent.Publish(workspaceName);
        
            viewModel.ActiveWorkspace = workspaceName;
 }
در این متد با استفاده از CPE که در پروژه Common ایجاد کردیم ماژول مورد نظر را لود خواهیم کرد. و بعد از آن مقدار ActiveWorkspace جاری در ViewModel به نام ماژول تغییر پیدا می‌کند. متد Publish در CPE این کار را انجام خواهد دارد.

عدم وابستگی ماژول ها
همان طور که می‌بینید ماژول‌های پروژه به هم Reference داده نشده اند حتی هیچ Reference هم به پروژه اصلی یعنی جایی که فایل App.xaml قرار دارد، داده نشده است ولی در عین حال باید با هم در ارتباط باشند. برای حل این مسئله این ماژول‌ها باید در فولدر bin پروژه اصلی خود را کپی کنند. بهترین روش استفاده از Pre-Post Build Event خود VS.Net است. برای این کار از پنجره Project Properties وارد برگه Build Events شوید و از قسمت Post Build Event Command Line  استفاده کنید و کد زیر را در آن کپی نمایید:
xcopy "$(TargetDir)FirstPrismSample.ModuleBook.dll" "$(SolutionDir)FirstPrismSample\bin\$(ConfigurationName)\Modules\" /Y
قطعا باید به جای FirstPrismSample نام Solution خود و به جای ModuleBook نام ماژول را وارد نمایید.

مانند:


مراحل بالا برای هر ماژول باید تکرار شود(ModuleNavigation , ModuleBook , ModuleCategory). بعد از Rebuild  پروژه در فولدر bin پروژه اصلی یک فولدر به نام Module ایجاد می‌شود که اسمبلی هر ماژول در آن کپی خواهد شد.

ایجاد Bootstrapper
حال نوبت به Bootstrapper میرسد(در پست قبلی در باره مفهوم Bootstrapper شرح داده شد). در پروژه اصلی یعنی جایی که فایل App.xaml قرار دارد کلاس زیر را ایجاد کنید.
    public class Bootstrapper : UnityBootstrapper
    {     
        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();
            Container.RegisterType<IModuleServices, ModuleServices>();
        }
   
        protected override DependencyObject CreateShell()
        {
            var shell = new Shell();
            shell.Show();
            return shell;
        }
   
        protected override IModuleCatalog GetModuleCatalog()
        {           
            var catalog = new DirectoryModuleCatalog();
            catalog.ModulePath = @".\Modules";
            return catalog;
        }
    }
متد ConfigureContainer برای تزریق وابستگی به وسیله UnityContainer استفاده می‌شود. در این متد باید تمامی Registration‌های مورد نیاز برای DI را انجام دهید. نکته مهم این است که عملیات وهله سازی و Initialization برای  Container  در متد base کلاس UnityBootstrapper انجام خواهد شد پس همیشه باید متد base این کلاس در ابتدای این متد فراخوانی شود در غیر این صورت با خطا متوقف خواهید شد.
متد CreateShell برای ایجاد و وهله سازی از Shell پروژه استفاده می‌شود. در این جا یک وهله از Shell Window برگشت داده می‌شود.
متد GetModuleCatalog برای تعیین مسیر ماژول‌ها در پروژه کاربرد دارد. در این متد با استفاده از خاصیت ModulePath کلاس DirectoryModuleCatalog تعیین کرده ایم که ماژول‌های پروژه در فولدر Modules موجود در bin اصلی پروژه قرار دارد. اگر به دستورات کپی در Post Build Event قسمت قبل توجه کنید می‌بینید که دستور ساخت فولدر وجود دارد.
"$(SolutionDir)FirstPrismSample\bin\$(ConfigurationName)\Modules\" /Y
*نکته: اگر استفاده از این روش برای شناسایی ماژول‌ها توسط Bootstrapper را چندان جالب نمی‌دانید می‌تونید از MEF استفاده کنید که اسمبلی ماژول‌های پروژه را به راحتی شناسایی می‌کند و در اختیار Bootsrtapper قرار می‌دهد(از آن جا در مستندات مربوط به Prism، بیشتر به استفاده از MEF تاکید شده است من هم در پست‌های بعدی، مثال‌ها را با MEF پیاده سازی خواهم کرد)

در پایان باید فایل App.xaml را تغییر دهید به گونه ای که متد Run در کلاس Bootstapper ابتدا اجرا شود.
public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            var bootstrapper = new Bootstrapper();
            bootstrapper.Run();
        }
    }


اجرای پروژه:
بعد از اجرا، با انتخاب ماژول مورد نظر اطلاعات ماژول در Workspace Content Control لود خواهد شد.

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