مطالب
یکپارچه سازی CKEditor با Lightbox
در یک پروژه من احتیاج داشتم تا عکس هایی که کاربر از طریق ckeditor آپلود می‌کنه ، به صورت خودکار با lightbox  یکپارچه شه و به صورت گالری عکس نمایش داده شود.
همان طور که می‌دونید مکانیزم عملکرد اکثر پلاگین‌های lightbox  به صورت زیر است:
<a href="orginal size of image directory"><img src="thumb(smaller size) image directory" /></a>

خب معلوم میشه که آدرس عکسی که به صورت کوچیک)اصطلاحا بند انگشتی) به نمایش در میاد باید داخل تگ img ، وآدرس عکس با سایز اصلی ، داخل تگ a قرار بگیره.

پس تقریبا معلوم شد که چه باید بکنیم.

ابتدا باید عکسی که آپلود شده را به صورت خودکار به اندازه کوچکتر تغییر ابعاد داده و سپس آدرس عکس تغییر ابعاد داده شده ، را به عنوان آدرس عکس آپلود شده به ckeditor باز گردانیم  که در شکل زیر آن را نشان داده ام:

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


خب تا اینجا خلاصه ای از آنچه باید انجام شود را گفتم.

در کل منطقی که باید پیاده شود بدین گونه است:

1-  پیاده سازی ckeditor
2-  فعال کردن قسمت آپلود عکس ckeditor
3-  آپلود کردن عکس و تغییر اندازه آن به کمک  کلاس WebImage
4-  برگرداندن آدرس عکس‌های آپلود شده به ckeditor و روش‌های پیاده سازی آن

نکات:
الف) من در اینجا از ASP.NET MVC استفاده می‌کنم .شما  نیز می‌توانید منطق پیاده سازی شده را به راحتی و با کمی تغییرات به پروژه خود اعمال کنید.
ب) کدهایی که من نوشتم 100% بهینه نیستند و کاملا هم بدون اشکال نیستند ولی  نیازهای شما را برآورده می‌کند.
ج)آدرس دادن فایل‌های من کاملا صحیح نیستند و بهتر است شما از T4MVC استفاده کنید.

مرحله اول: دانلود و پیاده سازی ckeditor در صفحه

ابتدا به سایت ckeditor.com مراجعه  و از قسمت دانلود، آن را دانلود کرده و سپس از حالت فشرده درآورده، در فولدری مثلا به نام Scripts در پروژه‌ی خود بریزید.
همان طور که می‌دانید ckeditor با یک textarea  یکپارچه میشود ، که من ساده‌ترین آن را برای شما شرح می‌دهم.
اول از همه شما باید یک فایل جاوا اسکریپت متعلق به ckeditor  را در صفحه ای که از آن می‌خواهید استفاده کنید به صفحه ضمیمه کنید. نام این فایل ckeditor.js هست که در فایل دانلودی در داخل پوشه‌ی ckeditor دیده میشود.
 
<script src="/Scripts/ckeditor/ckeditor.js" type="text/javascript"></script>
 نکته: بهتر است که این ارجاع را قبل از بسته شدن تگ body انجام دهید یعنی درست قبل از </body>.
خب فرض کنید که یک textarea به شکل زیر داریم. برای اینکه این textarea  با ckeditor  یکپارچه شود کافیست که class آن را بر روی ckeditor  قرار دهید.
<textarea id="content" name="content" class="ckeditor" ></textarea>
نکته: حتما برای textarea خود id را نیز تعیین کنید ، چون در غیر این صورت  ckeditor کار نخواهد کرد.

تا به اینجای کار توانستیم ckeditor را اجرا کنیم.
در مرحله بعد سراغ فعال سازی آپلود عکس در آن می‌رویم.

مرحله دوم: فعال سازی آپلود عکس در ckeditor

از شواهد این طور به نظر میاد که به صورت پیشفرض این امکان غیر فعال است و باید به صورت دستی آن را فعال کنیم.
 برای این کار کافیست که از مستندات ckeditor  کمک بگیریم.
خب یکی از راحت‌ترین این روش‌ها این است که قبل از بسته شدن تگ بادی فایل جاوا اسکریپت زیر را بنویسیم:
 
<script type="text/javascript">
        CKEDITOR.replace( 'content' , { filebrowserImageUploadUrl: '/Admin/UploadImage' } );
</script>
نکته اول: این خط کدی که نوشتیم مرحله اول را باطل می‌کنه یعنی احتیاج نبود که مرحله اول را طی کنیم و می‌توانستیم مستقیما از همین مرحله شروع کنیم.
توضیحات:
آرگومان اول CKEDITOR.replace که در اینجا content  است ، در واقع  id همان textarea ای هست که می‌خواهیم ckeditor  روی آن اعمال شود.
آرگومان دوم نام کنترلر و اکشن را برای آپلود فایل مشخص می‌کنه.(از منطق ASP.Net MVC استفاده کردم)
خب تا اینجا اگر تست بگیرید میبینید که قسمت زیر برایتان فعال شده.

مرحله سوم: آپلود کردن عکس و تغییر اندازه آن به کمک  کلاس WebImage

برای این که ببینم که این فرم آپلود ckeditor ، چه پارامترهایی را به اکشن متد من ارسال می‌کنه ، از فایرباگ کمک گرفتم:

همان طور که می‌یبینید به خوبی آدرس post  شدن اطلاعات به اکشن متدی که من برایش مشخص کردم را فهمیده.
اما سه پارامتر دیگر نیز به اکشن متد ما ارسال می‌کنه: CKEditor و CKeditorFuncNum و langCod
برای اینکه با این پارامتر‌های ارسالی بیشتر آشنا شوید ، توصیه می‌کنم این صفحه را ببینید.
آنچه که از این پارامتر‌ها برای ما مهم هست ، من در اکشن متد تعریفی خود لحاظ کرده ام.

خب همون طور که یپش از این گفته بودم ما نیاز داریم که عکس آپلود شده را به ابعاد کوچکتر تغییر اندازه داده  و اصطلاحا از آن به عنوان تصویر بند انگشتی استفاده کنیم. آدرس این عکس کوچک شده ، همانیست که در آدرس تگ img  قرار می‌گیره و در ابتدا به کاربر نمایش داده میشه.
برای انجام این عمل من کلاسی را برای کار کردن با عکس برایتان معرفی می‌کنم،  به نام WebImage  که  در داخل فضای نامی System.Web.Helpers  قرار گرفته است.
از طریق این کلاس می‌توان کلیه عملیات دریافت فایل آپلود شده،  ویرایش ، تغییر اندازه ، چرخاندن ، بریدن و حتی watermark کردن و در نهایت ذخیره عکس را به آسانی انجام داد.
من کدهای متد UploadImage را برایتان قرار می‌دهم که زیاد هم بهینه نیست و سپس برایتان توضیح میدهم.

public ActionResult UploadImage(string CKEditorFuncNum, string CKEditor, string langCode)
        {
            string message;  // message to display   when file upload successfully(optional)
            string thumbPath = "";  // the directory for thumb file that should resize
            var db = new MyDbContext();  // make new instance from my context
            // here logic to upload image
            // and get file path of the image
            var file = WebImage.GetImageFromRequest(); // get the uploaded file from request
            var ext = Path.GetExtension(file.FileName);  // get the path of file
            //get the file name without extension
            var fileName = Path.GetFileNameWithoutExtension(file.FileName);
            //add time to file name to avoid same name file overwrite, and then add extension to it
            fileName += DateTime.Now.ToFileTime() + ext;
            //choose the path for the original size of image
            var path = Path.Combine(Server.MapPath("~/Content/UploadedImages/Default"), fileName);
            file.Save(path);  //save the original size of the image
            db.Images.Add(new Image { RealName = file.FileName, FileName = fileName, UploadDate = DateTime.Now }); // save image info to db
            db.SaveChanges();  // submit changes to db
            string defaultPath = "/Content/UploadedImages/Default/" + fileName;  //path for original size of //images
            if (file.Width > 400)  // if width of image bigger than 400 px do resize
            {
                file.Resize(400, 400, true);  //resize the image , third argument is aspect ratio
                string thumbName = "Thumb-" + fileName;  // resized image name
                //path for resized image file
                string path2 = Server.MapPath("~/Content/UploadedImages/Thumbs/" + thumbName);
                thumbPath = "/Content/UploadedImages/Thumbs/" + thumbName;
                //save resized image file
                file.Save(path2);
            }
            else
            {
                thumbPath = defaultPath;  // if the size not bigger than 400px the thumb, path = default path
            }
            // passing message success/failure
            message = "Image was saved correctly";
            // since it is an ajax request it requires this string
            //java script that return files path to ckeditor
            string output = @"<script>window.parent.CKEDITOR.tools.callFunction(" + CKEditorFuncNum + ", \"" + thumbPath + "\", \"" + message + "\");window.parent.document.getElementById('cke_145_textInput').value='" + defaultPath + "';window.parent.document.getElementById('cke_125_textInput').value=0;</script>";
            return Content(output);
        }


توضیحات:
ابتدا یه رشته  به نام message  در نظر گرفتم ، برای هنگامی که آپلود شد، ckeditor به کاربر نشان بده.
سپس منطق من به این صورت بوده که مسیری برای ذخیره سازی فایل‌های تغییر اندازه داده شده ، و نیز مسیری برای فایل‌های با اندازه اصلی در نظر گرفتم.
همچنین من در اینجا من از بانک اطلاعاتی برای ذخیره سازی اطلاعاتی از عکس استفاده کردم،  که در اینجا بحث اصلی ما نیست.
سپس به کمک WebImage.GetImageFromRequest فایل آپلود شده را دریافت کردم.این متد به اندازه کافی باهوش هست که بفهمد ، چه فایلی آپلود شده.
سپس پسوند فایل را از نام فایل جدا کردم، و تاریخ کنونی را به شکل رشته در آورده  و به انتهای نام عکس اضافه کرده تا از تکراری نبودن نام عکس‌ها مطمئن باشم.
سپس پسوند فایل را نیز دوباره به نام فایل اضافه کردم و به کمک متد Save کلاس WebImage عکس را ذخیره کردم.
سپس چک کردم که اگر عرض عکس بیشتر از 400  پیکسل هست ، آن  را تغییر اندازه  بده و ذخیره کنه، و در غیر این صورت آدرس عکسی که قرار بود تغییر اندازه داده بشه با آدرس عکس اصلی یکی میشه.


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

window.parent.CKEDITOR.tools.callFunction(" + CKEditorFuncNum + ", \"" + thumbPath + "\", \"" + message + "\");

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


همان طور که معلومه  id این فیلد cke_145_textInput  هست و به کمک یه خط js می‌توان این فیلد را با آدرس عکس آپلود شده با سایز اصلی پر کرد.
اولش من این را نوشتم:

document.getElementsById("cke_145_textInput").value = defaultPath;

اما بازم js  شروع کرد به بدقلق شدن. هرچی توی کنسول دیباگش کردم خطای null بودن را میداد. بعد از یه ساعت سرو کله زدن و تقریبا ناامید شدن ، چشمم به قسمت اول کد api خود ادیتور که اولش را با window.parent شروع کرده  افتاد ، و من هم کد خودم را به شکل زیر تغییر دادم:

window.parent.document.getElementsById("cke_145_textInput").value = defaultPath;

موفقیت در این قسمت از کد باعث شد که من دست به کد‌تر شوم و مشکل border عکس ها، که به صورت  دیفالت در IE  یا همون دشمن همیشگی وجود داره ، را حل کنم ومقدار border را به صورت پیش فرض صفر کنم. 

window.parent.document.getElementsById('cke_125_textInput').value=0;

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

$(".container [href$='.jpg']").colorbox({ maxWidth: 800, opacity: 0.5, rel: 'gal' });
$(".container [href$='.png']").colorbox({ maxWidth: 800, opacity: 0.5, rel: 'gal' });
$(".container [href$='.gif']").colorbox({ maxWidth: 800, opacity: 0.5, rel: 'gal' });

فرض کنید div ی که متن و عکس‌های ما را شامل میشه ، کلاسش container  باشه . با کمک [href$='.jpg']  می‌توان گفت هر لینکی که،  پسوند فایلی که به آن اشاره می‌کند، .jpg هست،  ویژگی colorbox  را به خود بگیرید.


یک پیشنهاد برای تشکیل گالری عکس:
همان طور که من در بالا اشاره کردم ، rel را بر روی  gal قرار دادم، تا هر تگی که ویژگی  rel را داشته باشد، تشکیل یک گروه برای گالری عکس را بدهد.
برای اینکه  بتوانیم این ویژگی را به عناصر مورد نظر خود اعمال کنیم ، بازم دست به دامان jQuery می‌شویم:

$(".container [href$='.jpg']").attr("rel", "gal");

خب مثل اینکه دیگر کار تمام شده و امیدوارم برای شما مفید بوده باشه.
موفق باشید... 

مطالب دوره‌ها
استفاده از StructureMap جهت تزریق وابستگی‌ها در برنامه‌های WPF و الگوی MVVM
در این قسمت قصد داریم همانند کنترلرها در ASP.NET MVC، کار تزریق وابستگی‌ها را در متدهای سازنده ViewModelهای WPF بدون استفاده از الگوی Service locator انجام دهیم؛ برای مثال:
    public class TestViewModel
    {
        private readonly ITestService _testService;
        public TestViewModel(ITestService testService) //تزریق وابستگی در سازنده کلاس
        {
            _testService = testService;
        }
و همچنین کار اتصال یک ViewModel، به View متناظر آن‌را نیز خودکار کنیم. قراردادی را نیز در اینجا بکار خواهیم گرفت:
نام تمام Viewهای برنامه به View ختم می‌شوند و نام ViewModelها به ViewModel. برای مثال TestViewModel و TestView معرف یک ViewModel و View متناظر خواهند بود.


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

namespace DI07.Services
{
    public interface ITestService
    {
        string Test();
    }
}

namespace DI07.Services
{
    public class TestService: ITestService
    {
        public string Test()
        {
            return "برای آزمایش";
        }
    }
}
یک پروژه WPF را آغاز کرده و سپس یک پروژه Class library دیگر را به نام Services با دو کلاس و اینترفیس فوق، به آن اضافه کنید. هدف از این کلاس‌ها صرفا آشنایی با نحوه تزریق وابستگی‌ها در سازنده یک کلاس ViewModel در WPF است.


علامتگذاری ViewModelها

در ادامه یک اینترفیس خالی را به نام IViewModel مشاهده می‌کنید:
namespace DI07.Core
{
    public interface IViewModel // از این اینترفیس خالی برای یافتن و علامتگذاری ویوو مدل‌ها استفاده می‌کنیم
    {
    }
}
از این اینترفیس برای علامتگذاری ViewModelهای برنامه استفاده خواهد شد. این روش، یکی از انواع روش‌هایی است که در مباحث Reflection برای یافتن کلاس‌هایی از نوع مشخص استفاده می‌شود.
برای نمونه کلاس TestViewModel برنامه، با پیاده سازی IViewModel، به نوعی نشانه گذاری نیز شده است:
using DI07.Services;
using DI07.Core;

namespace DI07.ViewModels
{
    public class TestViewModel : IViewModel // علامتگذاری ویوو مدل
    {
        private readonly ITestService _testService;
        public TestViewModel(ITestService testService) //تزریق وابستگی در سازنده کلاس
        {
            _testService = testService;
        }

        public string Data
        {
            get { return _testService.Test(); }
        }
    }
}


تنظیمات آغازین IoC Container مورد استفاده

در کلاس استاندارد App برنامه WPF خود، کار تنظیمات اولیه StructureMap را انجام خواهیم داد:
using System.Windows;
using DI07.Core;
using DI07.Services;
using StructureMap;

namespace DI07
{
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            ObjectFactory.Configure(cfg =>
            {
                cfg.For<ITestService>().Use<TestService>();

                cfg.Scan(scan =>
                {
                    scan.TheCallingAssembly();
                    // Add all types that implement IView into the container, 
                    // and name each specific type by the short type name.
                    scan.AddAllTypesOf<IViewModel>().NameBy(type => type.Name);
                    scan.WithDefaultConventions();
                });
            });
        }
    }
}
در اینجا عنوان شده است که اگر نیاز به نوع ITestService وجود داشت، کلاس TestService را وهله سازی کن.
همچنین در ادامه از قابلیت اسکن این IoC Container برای یافتن کلاس‌هایی که IViewModel را در اسمبلی جاری پیاده سازی کرده‌اند، استفاده شده است. متد NameBy، سبب می‌شود که بتوان به این نوع‌های یافت شده از طریق نام کلاس‌های متناظر دسترسی یافت.


اتصال خودکار ViewModelها به Viewهای برنامه

using System.Windows.Controls;
using StructureMap;

namespace DI07.Core
{
    /// <summary>
    /// Stitches together a view and its view-model
    /// </summary>
    public static class ViewModelFactory
    {
        public static void WireUp(this ContentControl control)
        {
            var viewName = control.GetType().Name;
            var viewModelName = string.Concat(viewName, "Model"); //قرار داد نامگذاری ما است
            control.Loaded += (s, e) =>
            {
                control.DataContext = ObjectFactory.GetNamedInstance<IViewModel>(viewModelName);
            };
        }
    }
}
اکنون که کار علامتگذاری ViewModelها انجام شده و همچنین IoC Container ما می‌داند که چگونه باید آن‌ها را در اسمبلی جاری جستجو کند، مرحله بعدی، ایجاد کلاسی است که از این تنظیمات استفاده می‌کند. در کلاس ViewModelFactory، متد WireUp، وهله‌ای از یک View را دریافت کرده، نام آن‌را استخراج می‌کند و سپس بر اساس قراردادی که در ابتدای بحث وضع کردیم، نام ViewModel متناظر را یافته و سپس زمانیکه این View بارگذاری می‌شود، به صورت خودکار DataContext آن‌را به کمک StructureMap وهله سازی می‌کند. این وهله سازی به همراه تزریق خودکار وابستگی‌ها در سازنده کلاس ViewModel نیز خواهد بود.


استفاده از کلاس ViewModelFactory

در ادامه کدهای TestView و پنجره اصلی برنامه را مشاهده می‌کنید:

<UserControl x:Class="DI07.Views.TestView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <TextBlock Text="{Binding Data}" />
    </Grid>
</UserControl>


<Window x:Class="DI07.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Views="clr-namespace:DI07.Views"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Views:TestView />
    </Grid>
</Window>
در فایل Code behind مرتبط با TestView تنها کافی است سطر فراخوانی this.WireUp اضافه شود تا کار تزریق وابستگی‌ها، وهله سازی ViewModel متناظر و همچنین مقدار دهی DataContext آن به صورت خودکار انجام شود:
using DI07.Core;

namespace DI07.Views
{
    public partial class TestView
    {
        public TestView()
        {
            InitializeComponent();
            this.WireUp(); //تزریق خودکار وابستگی‌ها و یافتن ویوو مدل متناظر
        }
    }
}

دریافت پروژه کامل این قسمت
  DI07.zip
نظرات اشتراک‌ها
نگارش بعدی ASP.NET Core از Full .NET Framework پشتیبانی نمی‌کند
بحث اصلی مطلب جاری این است که NET Core. (پیاده سازی) با NET Standard. (قرارداد) یکی نیست. NET Core. یکی از پیاده سازی‌های NET Standard. است؛ مانند دات نت 4.6.1 که آن هم پیاده سازی کننده‌ی دات نت استاندارد 2 است. هر دوی این‌ها دارای یک سری API خاص خودشان هم هستند که در NET Standard. وجود ندارند. سطح API دات نت Core هم بیشتر است از NET Standard. و در یکسری از موارد هم کاملا ناسازگار با دات نت کامل. بنابراین توافقی که در اینجا وجود دارد، صرفا پیاده سازی قرارداد NET Standard. است و بیشتر از آن هم مربوط است به توسعه دهندگان آن سکوی کاری خاص که الزامی به یکی بودن آن‌ها نیست.
کاری که پیشتر می‌خواستند انجام دهند، محدود کردن ASP.NET Core به سطح API موجود در NET Core. بود (NET Core 2.0 only. یا تصویر زیر) و نه صرفا به NET Standard. . این مساله برای توسعه دهندگان ASP.NET Core می‌توانست فوق العاده باشد؛ چون سطح API بیشتر و به‌روزتری را در اختیارشان قرار می‌داد و اگر قابلیتی در NET Standard. وجود نداشت، نمی‌بایستی درخواست می‌دادند تا اضافه شود تا بعد بتوانند استفاده کنند.


اما چون این مساله و سطح API بیشتر و گاهی از اوقات کاملا متفاوت، سازگاری با کتابخانه‌هایی را که در میدان دید این API قرار نمی‌گرفتند، زیر سؤال می‌برد، ارتقاء برنامه‌های قبلی را با مشکل مواجه می‌کرد. به همین جهت تصمیم گرفته‌اند که ASP.NET Core را سازگار با دات نت فریم ورک کامل نیز ارائه دهند و بنابراین «محدودش کنند» به NET Standard 2.0.  و نه حالت NET Core only‌. قبلی: اطلاعات بیشتر


به علاوه باید درنظر داشت که امکان اضافه کردن یک بسته‌ی نیوگت از یک کتابخانه‌ی نوشته شده‌ی برای دات نت کامل در برنامه‌های دات نت Core به معنای تضمینی برای کار کردن آن در زمان اجرا نخواهد بود. از این جهت که دات نت کامل، به همراه قسمت‌هایی است که در NET Standard. وجود خارجی ندارند. بنابراین اگر کتابخانه‌ی استفاده شده صرفا این API مشترک را هدف قرار داده‌است، هم قابلیت اتصال و هم قابلیت اجرا را خواهد داشت؛ اما اگر برای مثال کسی بسته‌ی NServiceBus را به پروژه‌ی ASP.NET Core 2.0 اضافه کند، بدون مشکل کامپایل خواهد شد. اما از آنجائیکه این کتابخانه از MSMQ استفاده می‌کند که خارج از میدان دید این استاندارد است، در زمان اجرا با شکست مواجه خواهد شد.
مطالب
بررسی تفاوت Task و ValueTask

زمانیکه تصمیم میگیریم کدهای زده شده را بهینه کنیم، اکثرا دنبال راه حل‌های جدید نمیگردیم. این مورد کاملا غریزی است؛ چرا که به‌دنبال کم‌ترین انرژی و بیشترین بازدهی هستیم؛ این طبیعت انسان است. صرفا کدهای قبلی را بازبینی میکنیم و سعی میکنیم  نحوه‌ی نوشتن منطق‌های موجود را بهینه کنیم. در همین راستا درک عملکرد Task و ValueTask ‌ها شاید قدمی مهم در مورد بهینه کردن کد‌ها باشد؛ چرا استفاده درست و بجای این دو مورد می‌تواند تاثیر زیادی بر روی سرعت و استفاده از مصرف حافظه داشته باشد؟ در این مقاله سعی میکنیم تا درک درستی از این دو داشته باشیم.


Task<T>  چیست؟

Task یک کلاس در فضای نام System.Threading.Tasks است؛ به‌طوریکه کمک میکند تا یک قسمت از برنامه به صورت مستقل از Thread اصلی اجرا شود. به‌بیان دیگر می‌تواند یک Thread Pool را ایجاد و با توجه به روند کار، از یک مرحله‌ی اجرایی به مرحله‌ای دیگر منتقل می‌کند. همچنین هر Task می‌تواند یک مقدار برگشتی نیز داشته باشد.

 این درحالی‌است که می‌تواند صرفا یک فرآیند را اجرا کند، بدون اینکه خروجی داشته باشد. به‌عبارتی دیگر اگر فرآیندی داشته باشیم که در نهایت یک شناسه را برمیگرداند، از Task<int> و اگر فرآیندی داشته باشیم که صرفا فرآیند همگام سازی داده‌های قدیمی به جدید را انجام میدهد، می‌تواند از نوع Task باشد.

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

به دلیل اینکه Task یک class است و class ‌ها از نوع ReferenceType می‌باشند، روی حافظه‌ی Heap ذخیره می‌شوند و به‌ازای هر بار فراخوانی متدی که خروجی Task دارد، شیء Task را روی Heap ذخیره میکند. این شیء وضعیت اجرای قسمتی از کد ما را که میتواند sync یا async باشد، در خود ذخیره میکند تا در نهایت اجرای آن کامل شود.


نحوه استفاده از Task<T>

برای درک بهتر، یک تکه کد را با بهره بردن از Task ایجاد میکنیم :

public static class DummyWeatherProvider
{
    public static async Task<Weather> Get(string city)
    {
        await Task.Delay(10);
        var weather = new Weather 
        { 
            City = city, 
            Date = DateTime.Now, 
            AvgTempratureF = new Random().Next(5, 70) 
        };
        
        return weather;
    }
}
همان طور که مشخص است، کلاس موجود یک متد به نام Get دارد تا اطلاعات آب و هوای  شهر مورد نظر را به صورت یک Task  برگرداند. حال کد زیر را جهت بررسی تغییر وضعیت‌های اجرایی این Task ایجاد می‌کنیم :
static async Task CheckTaskStatus()
{
   var task = DummyWeatherProvider.Get("Stockholm");
    LogTaskStatus(task.Status);
    await task;
    LogTaskStatus(task.Status);
}

static void LogTaskStatus(TaskStatus status)
{
    Console.WriteLine($"Task Status: {Enum.GetName(typeof(TaskStatus), status)}");
}
TaskStatus یک enumeration است، به‌طوری‌که بیانگر وضعیت‌های مختلف یک Task در حال اجرا می‌باشد. برای مثال: WaitingForActivation, Running, RanToCompletion. در کد بالا ابتدا متد را فراخوانی می‌کنیم. سپس منتظر می‌مانیم تا متد اجرا شده، تکمیل شود. در اولین لاگ وضعیت، به WaitingForActivation و در دومین لاگ به RanToCompletion تبدیل میشود. حال‌که با Task ها و نحوه‌ی اجرای فرآیند آن آشنا شدیم، در قسمت بعدی به بررسی ValueTask ها می‌پردازیم. 

ValueTask<T>  چیست؟

همانند Task ، ValueTask هم برای مدیریت وضعیت فرآیند استفاده میشود؛ با این تفاوت که ValueTask ‌ها از نوع struct هستند. به‌طوریکه نحوه‌ی ذخیره سازی آن‌ها در حافظه به نسبت class ‌ها کاملا متفاوت است. از نقطه نظر سرعت، تشخیص دادن اینکه کدامیک باید استفاده شود، باید با توجه به سناریو، بررسی و انتخاب شود؛ چرا که از نظر تخصیص حافظه متفاوت عمل می‌کنند. برای درک بهتر عملکرد ValueTask ‌ها کد زیر را بررسی میکنیم :

public class WeatherService
{
    private readonly ConcurrentDictionary<string, Weather> _cache;
    public WeatherService()
    {
        _cache = new();
    }

    public async Task<Weather> GetWeatherTask(string city)
    {
        if (!_cache.ContainsKey(city))
        {
            var weather = await DummyWeatherProvider.Get(city);
            _cache.TryAdd(city, weather);
        }
        return _cache[city];
    }

    public async ValueTask<Weather> GetWeatherValueTask(string city)
    {
        if (!_cache.ContainsKey(city))
        {
            var weather = await DummyWeatherProvider.Get(city);
            _cache.TryAdd(city, weather);
        }
        return _cache[city];   
  }

کلاس WeatherService شامل یک فیلد private از نوع collection و دو متد است. ما از _cache  جهت نگهداری اطلاعاتی که قبلا دریافت شده، استفاده می‌کنیم و به نوعی in-memory cache را پیاده سازی میکنیم. پیاده سازی منطق هر دو متد  GetWeatherTask و GetWeatherValueTask  کاملا شبیه به هم است؛ به‌طوری‌که اول بررسی میکنیم اطلاعات آب و هوای شهر مورد نظر در _cache وجود دارد یا خیر؟ اگر وجود داشت، اطلاعات به صورت مستقیم برگشت داده می‌شود؛ در غیر این صورت DummyWeatherProvider.Get()  فراخوانی خواهد شد. 

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

برای مقایسه‌ی مصرف حافظه‌ی این دو روی هر دو متد، Benchmark میگیریم. برای پیاده سازی نیار به کد‌های زیر داریم : 

[MemoryDiagnoser]
public class TaskAndValueTaskBenchmark
{
    private readonly WeatherService _weatherService;
    public TaskAndValueTaskBenchmark()
    {
        _weatherService = new();
    }
    
    [Benchmark]
    [Arguments("Denver")]
    public async Task<Weather> TaskBenchmark(string city)
    {
        return await _weatherService.GetWeatherTask(city);
    }

    [Benchmark]
    [Arguments("London")]
    public async ValueTask<Weather> ValueTaskBenchmark(string city)
    {
        return await _weatherService.GetWeatherValueTask(city);
    }
}

نتیجه به دست آمده به شرح زیر است :

Allocated

Gen0

Method

144 B

0.0229

TaskBenchmark

------

----

ValueTaskBenchmark

  با توجه به نتیجه به‌دست آمده، متدی که خروجی ValueTask دارد، حافظه‌ای را تخصیص نداده‌است؛ این دقیقا مزیت مهم ValueTask نسبت به Task  می‌باشد.

مزیت  ValueTask<T>

به‌دلیل اینکه از نوع struct هستند، بر روی حافظه، در قسمت Stack ذخیره می‌شوند و به صورت خودکار بعد از اینکه نیازی به آنها نباشد، از حافظه حذف می‌شوند . به همین دلیل به شکل قابل توجهی، فشار را از روی GC کاهش می‌دهد .

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

این سری متد GetWeatherValueTask   را جهت تشخص اینکه  اغلب کدها به صورت sync یا async اجرا می‌شوند، بررسی می‌کنیم. در متد ذکر شده اگر اطلاعات شهر مورد نظر وجود داشته باشد، کار به صورت sync اجرا می‌شود و اگر شهر وجود نداشته باشد، کار به صورت async اجرا می‌شود. با بررسی دقیق‌تر متوجه می‌شویم اکثر مواقع در این متد کار به صورت sync  اجرا می‌شود؛ چرا که بعد ازدریافت اطلاعات، مجدد آن را دریافت نمیکند، بلکه از حافظه میخواند (همان _cache ) .


محدودیت‌های استفاده از    ValueTask<T>  

1. در اینجا تنها یکبار امکان استفاده از await وجود دارد. وقتی یکبار valueTask را await می‌کنیم، بهتر است کار دیگری بر روی آن انجام ندهیم؛ چراکه ممکن است از حافظه پاک شده باشد.

2. اگر در سناریویی لازم دارید چندین بار await را بر روی valueTask اجرا کنید، لازم است ابتدا آن را به Task تبدیل کنیم. برای این کار متد AsTask را فراخوانی میکنیم (بهتر است صرفا یکبار متد AsTask را فراخوانی کنیم).

3. نمیتوانیم به یک ValueTask به صورت هم زمان در حالت Multi threads دسترسی داشته باشیم.

4. به صورت پیش فرض خروجی عملیات async، نوع Task می‌باشد؛ مگر اینکه اغلب مراحل کار به صورت sync اجرا شود، مانند مثالی که بالاتر اشاره شد.


منابع :

مطالب
شروع کار با GraphQL در ASP.NET Core
در این مقاله هدف این است که GraphQL را در ASP.NET Core راه اندازی کنیم. از یک کتابخانه ثالث برای آسان‌تر کردن یکپارچگی استفاده می‌کنیم و همچنین با جزئیات، توضیح خواهیم داد که چگونه می‌توان از element‌های مربوط به GraphQL مثل (Type ،Query و Schema) برای کامل کردن فرآیند یکپارچگی در ASP.NET Core استفاده کنیم.

GraphQL  و تفاوت‌های آن با REST
GraphQl یک query language می‌باشد که query‌ها را با استفاده از type system‌‌‌هایی که ما برای داده‌ها تعریف می‌کنیم، اجرا می‌کند. GraphQL  به هیچ زبان یا پایگاه داده مشخصی گره نخورده است. 

  •  GraphQL نیازمند رفت و برگشت‌های کمتری به server، به منظور بازیابی داده‌ها برای template یا view است. همراه با REST ما مجبور هستیم که چندیدن endpoint مثلا  (... api/students, api/courses, api/instructors ) را برای گرفتن همه داده‌های که برای template یا view نیاز داریم ملاقات کنیم؛ ولی این شرایط در GraphQL برقرار نیست. با GraphQL ما تنها یک query را ایجاد می‌کنیم که چندین تابع (resolver) را در سمت سرور فراخوانی می‌کند و همه داده‌ها را از منابع مختلفی، در یک درخواست برگشت می‌دهد. 

  • همراه با REST، همانطور که Application ما رشد می‌کند، تعداد endpoint‌ها هم زیاد می‌شوند که این نیازمند زمان بیشتری برای نگهداری می‌باشد. اما با GraphQL ما تنها یک endpoint  داریم؛ همین! 
  • با استفاده از GraphQL، ما هرگز به مشکل گرفتن داده‌هایی کم یا زیاد از منبع روبرو نخواهیم شد. به این خاطر است که ما query‌ها را با فیلد‌هایی که چیز‌هایی را که نیاز داریم، نشان می‌دهند، تعریف می‌کنیم. در این صورت ما همیشه چیز‌هایی را که درخواست داده‌ایم، دریافت می‌کنیم.

بنابراین اگر یک query شبیه زیر را ارسال کنیم :
query OwnersQuery {
  owners {
    name
    account {
      type
    }
  } 
}
صد درصد مطمئن هستیم که خروجی زیر برگشت داده می‌شود:
{
  "data": {
    "owners": [
     {
      "name": "John Doe",
      "accounts": [
        {
          "type": "Cash"
        },
        {
          "type": "Savings"
        }
      ]
     }
    ]
  }
}

همراه با REST  این شرایط برقرار نمی‌باشد. بعضی از مواقع ما چیزی بیشتر یا کمتر از آنچه که نیاز داریم دریافت می‌کنیم؛ که این بستگی به اینکه چگونه action‌ها در یک endpoint مشخص، پیاده سازی شده‌اند، دارد.  

شروع کار 
یک پروژه جدید را با استفاده از دستور زیر ایجاد می‌کنیم: 
dotnet new api -n ASPCoreGraphQL
سپس ساختار زیر را ایجاد می‌کنیم :


پوشه Contracts شامل واسط‌های مورد نیاز برای repository logic می‌باشد:

namespace ASPCoreGraphQL.Contracts
{
    public interface IOwnerRepository
    {
    }
}
namespace ASPCoreGraphQL.Contracts
{
    public interface IAccountRepository
    {
    }
}


در پوشه Models، کلاس‌های مدل را نگه داری می‌کنیم؛ به همراه یک کلاس context و کلاس‌های configuration:
public class Owner
{
    [Key]
    public Guid Id { get; set; }
    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; }
    public string Address { get; set; }
 
    public ICollection<Account> Accounts { get; set; }
}

public class Account
{
    [Key]
    public Guid Id { get; set; }
    [Required(ErrorMessage = "Type is required")]
    public TypeOfAccount Type { get; set; }
    public string Description { get; set; }
 
    [ForeignKey("OwnerId")]
    public Guid OwnerId { get; set; }
    public Owner Owner { get; set; }
}

public enum TypeOfAccount
{
    Cash,
    Savings,
    Expense,
    Income
}
    public class ApplicationContext : DbContext
    {
        public ApplicationContext(DbContextOptions options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

        }

        public DbSet<Owner> Owners { get; set; }
        public DbSet<Account> Accounts { get; set; }
    }

در پوشه Repository، کلاس‌های مرتبط با منطق بازیابی داده‌ها را داریم:
public class OwnerRepository : IOwnerRepository
{
    private readonly ApplicationContext _context;
 
    public OwnerRepository(ApplicationContext context)
    {
        _context = context;
    }
}
public class AccountRepository : IAccountRepository
{
    private readonly ApplicationContext _context;
 
    public AccountRepository(ApplicationContext context)
    {
        _context = context;
    }
}
repository logic، یک راه اندازی ابتدایی بدون هیچ لایه اضافه‌تری است.

کلاس context و کلاس‌های repository، در فایل Startup.cs ثبت می‌شوند:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationContext>(opt =>
         opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); 
 
    services.AddScoped<IOwnerRepository, OwnerRepository>();
    services.AddScoped<IAccountRepository, AccountRepository>();
 
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
        .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
}


Integration of GraphQL in ASP.NET Core 
برای کار کردن با GraphQL در ابتدا نیاز است که کتابخانه GraphQL را نصب کنیم. به همین منظور در ترمینال مربوط به VS Code، دستور زیر را وارد می‌کنیم: 
dotnet add package GraphQL
و همچنین کتابخانه‌ی زیر که به ما کمک می‌کند تا GraphQL.NET را به عنوان یک وابستگی دریافت کنیم:
dotnet add package GraphQL.Server.Transports.AspNetCore
و در نهایت نصب کتابخانه زیر که کمک می‌کند query‌های GraphQL را به سرور ارسال کنیم:
dotnet add package GraphQL.Server.Ui.Playground

(Creating GraphQL Specific Objects (Type, Query, Schema

کار را با ایجاد کردن یک پوشه جدید به نام GraphQL و سپس در آن ایجاد یک پوشه دیگر به نام GraphQLSchema، شروع می‌کنیم. در پوشه GraphQLSchema، یک کلاس را به نام AppSchema، ایجاد می‌کنیم.
کلاس AppSchema باید از کلاس Schema ارث بری کند تا در فضای نام GraphQL.Types قرار گیرد. در سازنده این کلاس، IDependencyResolver را تزریق می‌کنیم که قرار است به ما کمک کند تا اشیاء Query ،Mutation یا Subscription را resolve کنیم:
public class AppSchema : Schema
{
    public AppSchema(IDependencyResolver resolver)
        :base(resolver)
    {
 
    }
}
چیزی که مهم است بدانیم این است که خصوصیات schema، مثل Query ،Mutation و Subscription، واسط IObjectGraphType را پیاده سازی می‌کنند تا اشیائی را که قرار است resolve کنیم، همین type را پیاده سازی کرده باشند. در ضمن GraphQL API نمی‌تواند مدل را به عنوان نتیجه به صورت مستقیم بازگشت دهد.
فعلا این کلاس را در همین حالت می‌گذاریم و سپس یک پوشه را به نام GraphQLTypes در پوشه GraphQL ایجاد می‌کنیم. در پوشه GraphQLTypes یک کلاس را به نام OwnerType ایجاد می‌کنیم:
public class OwnerType : ObjectGraphType<Owner>
{
    public OwnerType()
    {
        Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object.");
        Field(x => x.Name).Description("Name property from the owner object.");
        Field(x => x.Address).Description("Address property from the owner object.");
    }
}
از کلاس OwnerType به عنوان یک جایگزین برای مدل Owner درون یک GraphQL API استفاده می‌کنیم. این کلاس از نوع جنریک ObjectGraphType ارث بری می‌کند. با متد Field، فیلد‌هایی را که بیانگر خصوصیات مدل Owner می‌باشند، مشخص می‌کنیم.
در ابتدا واسط IOwnerRepository و کلاس  OwnerRepository را به حالت زیر ویرایش می‌کنیم:
public interface IOwnerRepository
{
    IEnumerable<Owner> GetAll();
}


public class OwnerRepository : IOwnerRepository
{
    private readonly ApplicationContext _context;
 
    public OwnerRepository(ApplicationContext context)
    {
        _context = context;
    }
 
    public IEnumerable<Owner> GetAll() => _context.Owners.ToList();
}

در ادامه یک پوشه دیگر را به نام GraphQLQueries در پوشه‌ی GraphQL ایجاد و سپس در آن یک کلاس را به نام AppQuery ایجاد و آن را به حالت زیر ویرایش می‌کنیم:
public class AppQuery : ObjectGraphType
{
    public AppQuery(IOwnerRepository repository)
    {
        Field<ListGraphType<OwnerType>>(
           "owners",
           resolve: context => repository.GetAll()
       );
    }
}

توضیحات AppQuery
همانطور که می‌بینیم این کلاس از ObjectGraphType ارث بری می‌کند. در سازنده کلاس، IOwnerRepository را تزریق می‌کنیم و یک فیلد را به منظور برگشت دادن نتیجه برای یک Query مشخص، ایجاد می‌کنیم. در این کلاس، از نوع جنریک متد Field، استفاده کرده‌ایم که تعدادی type را به عنوان یک پارامتر جنریک پذیرش می‌کند که این بیانگر GraphQL.NET برای type ‌های معمول در NET. می‌باشد. علاوه بر ListGraphType،  نوع‌هایی مثل IntGraphType  و StringGraphType و ... وجود دارند (لیست کامل).
پارامتر owners نام فیلد می‌باشد (query مربوط به کلاینت باید با این نام مطابقت داشته باشد) و پارامتر دوم نتیجه می‌باشد.
بعد از انجام این مقدمات، اکنون کلاس AppSchema  را باز می‌کنیم و به حالت زیر آن را ویرایش می‌کنیم:
public class AppSchema : Schema
{
    public AppSchema(IDependencyResolver resolver)
        :base(resolver)
    {
        Query = resolver.Resolve<AppQuery>();
    }
}

Libraries and Schema Registration 
 در کلاس Startup نیاز است کتابخانه‌های نصب شده و هم چنین  کلاس schema ایجاد شده را ثبت کنیم. این کار را با ویرایش کردن متد ConfigureServices  و Configure  انجام میدهیم:
public void ConfigureServices(IServiceCollection services)
{
    ...
 
    services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
    services.AddScoped<AppSchema>();
 
    services.AddGraphQL(o => { o.ExposeExceptions = false; })
        .AddGraphTypes(ServiceLifetime.Scoped);

   ...
}
در متد ConfigureServices، در ابتدا DependencyResolver و سپس کلاس schema را ثبت می‌کنیم. علاوه بر آن، GraphQL را با متد AddGraphQL ثبت می‌کنیم. سپس همه تایپ‌های GraphQL را با متد AddGraphTypes ثبت می‌کنیم. بدون این متد (AddGraphTypes)، ما مجبور بودیم که همه type ‌ها و query ‌ها را به صورت دستی در APIامان ثبت کنیم.
در نهایت در متد schema  Configure را به خط لوله درخواست‌ها (request’s pipeline) اضافه می‌کنیم و هم چنین ابزار Playground UI:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   ...
    app.UseGraphQL<AppSchema>();
    app.UseGraphQLPlayground(options: new GraphQLPlaygroundOptions());
    app.UseMvc();
}

برای آزمایش GraphQL API امان، از ابزار GraphQL.UI.Playground استفاده می‌کنیم. در ابتدا پروژه را با دستور زیر اجرا می‌کنیم:
dotnet run
و سپس در مرورگر به این آدرس می‌رویم: 
https://localhost:5001/ui/playground
اکنون یک query را با نام owners ارسال می‌کنیم (این نام باید با نام query که در AppQuery در نظر گرفتیم، مطابقت داشته باشد). سپس نتیجه لازم را دریافت می‌کنیم. همانطور که مشاهده می‌کنیم، همه چیز بر اساس انتظاری که می‌رفت کار می‌کند. 



در قسمت بعد درابطه با  کوئری‌های پیشرفته، Error Handling و Data Loader در GraphQL  صحبت خواهیم کرد. 

کد‌های مربوط به این قسمت را از اینجا دریافت کنید .ASPCoreGraphQL.zip
مطالب
آموزش Prism #1
امروزه تقریبا تمام کسانی که پروژه‌های WPF یا Silverlight رو توسعه می‌دهند با مدل برنامه نویسی MVVM آشنایی دارند. فریم ورک‌های مختلفی برای توسعه پروژه‌ها به صورت MVVM وجود دارد. نظیر:
  • MVVM Light
  • Prism
  • Caliburn
  • Cinch
  • WAF
  • Catel
  • Onyx
  • MVVM helpers
  • و...

هر کدوم از فریم ورک‌های بالا مزایا، معایب و طرفداران خاص خودشون رو دارند(^) ولی به جرات می‌تونیم Prism رو به عنوان قوی‌ترین فریم ورک برای پیاده سازی پروژهای بزرگ و قوی و ماژولار با تکنولوژی WPF یا Silverlight بنامیم. در این پست به معرفی و بررسی مفاهیم اولیه Prism خواهیم پرداخت و در پست‌های دیگر به پیاده سازی عملی همراه با مثال می‌پردازیم.

*اگر به هر دلیلی مایل به یادگیری و استفاده از Prism نیستید، بهتون پیشنهاد می‌کنم از WAF استفاده کنید.

پیش نیازها:

برای یادگیری PRISM ابتدا باید با مفاهیم زیر در WPF یا Silverlight آشنایی داشته باشید.(فرض بر این است که به UserControl و Xaml و Dependency Properties، تسلط کامل دارید)

  • Data binding 
  • Resources
  • Commands
  • Behaviors

چرا Prism ؟

  • Prism به صورت کامل از Modular Programming برای پروژه‌های WPF و Silverlight پشتیانی می‌کند*
  • از Prism هم می‌توانیم در پروژه‌های WPF استفاده کنیم و هم Silverlight.
  • Prism به صورت کامل از الگوی MVVM برای پیاده سازی پروژه‌ها پشتیبانی می‌کند.
  • پیاده سازی مفاهیمی نظیر Composite Command و Command Behavior و Asynchronous Interacion  به راحتی در Prism امکان پذیر است.
  • مفاهیم تزریق وابستگی به صورت توکار در Prism فراهم است که برای پیاده سازی این مفاهیم  به طور پیش فرض امکان استفاده از UnityContainer و MEF در Prism تدارک دیده شده است.
  • پیاده سازی Region navigation در Prism به راحتی امکان پذیر است.
  • به وسیله امکان Event Aggregation به راحتی می‌توانیم بین ماژول‌های مختلف ارتباط برقرار کنیم.

*توضیح درباره برنامه‌های ماژولار

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

Prism امکاناتی رو برای طراحی و توسعه این گونه پروژه‌ها به صورت ماژولار فراهم کرده است:

  • ابتدا باید نام و مکان هر ماژول رو به Prism معرفی کنیم که می‌تونیم اون‌ها رو در کد یا Xaml یا Configuration File تعریف کنیم.
  • با استفاده از Metadata باید وابستگی‌ها و مقادیر اولیه برای هر ماژول مشخص شود.
  • با کمک تزریق وابستگی‌ها ارتباطات بین ماژول‌ها میسر می‌شود.
  • ماژول مورد نظر به دو صورت OnDemand و Available  لود خواهد شد.

در شکل زیر مراحل بالا قابل مشاهده است:

Bootstrapper چیست؟

در هر پروژه ماژولار (مختص Prism نیست) برای اینکه ماژول‌های مختلف یک پروژه، قابلیت استفاده به صورت یک پارچه رو در یک Application  داشته باشند باید مفهومی به نام Bootstapper رو پیاده سازی کنیم که وظیفه اون شناسایی و پیکربندی و لود ماژول هاست. در Prism دو نوع Bootstrapper پیش فرض وجود دارد.

  1. MefBootstrapper : کلاس پایه Bootstrapper که مبنای آن MEF است. اگر قصد استفاده از MEF رو در پروژه‌های خود دارید(^) Bootstrapper شما باید از این کلاس ارث ببرد.
  2. UnityBootstrapper : کلاس پایه Bootstrapper که مبنای آن UnityContainer است. اگر قصد استفاده از UnityContainer یا Service Locator (^) رو در پروژه‌های خود دارید Bootstrapper شما باید از این کلاس ارث ببرد. 

تصویری از ارتباط Bootstrapper با ماژول‌های سیستم

مفهوم Shell

در پروژه‌های WPF، در فایل App.xaml توسط یک Uri نقطه شروع پروژه را تعیین می‌کنیم. در پروژه‌های Silverlight به وسیله خاصیت RootVisual نقطه شروع سیستم تعیین می‌شود. در Prism نقطه شروع پروژه توسط bootsrapper تعیین می‌شود. دلیل این امر این است که Shell در پروژه‌های مبتنی بر Prism متکی بر Region Manager است. از Region  برای لود و نمایش ماژول‌ها استفاده خواهیم کرد.

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

مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت چهارم - به روز رسانی خودکار توکن‌ها
در قسمت قبل، عملیات ورود به سیستم و خروج از آن‌را تکمیل کردیم. پس از ورود شخص به سیستم، هربار انقضای توکن دسترسی او، سبب خواهد شد تا وقفه‌ای در کار جاری کاربر، جهت لاگین مجدد صورت گیرد. برای این منظور، قسمتی از مطالب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»  و یا «پیاده سازی JSON Web Token با ASP.NET Web API 2.x» به تولید refresh_token در سمت سرور اختصاص دارد که از نتیجه‌ی آن در اینجا استفاده خواهیم کرد. عملیات به روز رسانی خودکار توکن دسترسی (access_token در اینجا) سبب خواهد شد تا کاربر پس از انقضای آن، نیازی به لاگین دستی مجدد نداشته باشد. این به روز رسانی در پشت صحنه و به صورت خودکار صورت می‌گیرد. refresh_token یک guid است که به سمت سرور ارسال می‌شود و برنامه‌ی سمت سرور، پس از تائید آن (بررسی صحت وجود آن در بانک اطلاعاتی و سپس واکشی اطلاعات کاربر متناظر با آن)، یک access_token جدید را صادر می‌کند.


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

در مطلب «ایجاد تایمرها در برنامه‌های Angular» با روش کار با تایمرهای reactive آشنا شدیم. در اینجا قصد داریم از این امکانات جهت پیاده سازی به روز کننده‌ی خودکار access_token استفاده کنیم. در مطلب «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی»، زمان انقضای توکن را به کمک کتابخانه‌ی jwt-decode، از آن استخراج کردیم:
  getAccessTokenExpirationDateUtc(): Date {
    const decoded = this.getDecodedAccessToken();
    if (decoded.exp === undefined) {
      return null;
    }
    const date = new Date(0); // The 0 sets the date to the epoch
    date.setUTCSeconds(decoded.exp);
    return date;
  }
اکنون از این زمان در جهت تعریف یک تایمر خود متوقف شونده استفاده می‌کنیم:
  private refreshTokenSubscription: Subscription;

  scheduleRefreshToken() {
    if (!this.isLoggedIn()) {
      return;
    }

    this.unscheduleRefreshToken();

    const expiresAtUtc = this.getAccessTokenExpirationDateUtc().valueOf();
    const nowUtc = new Date().valueOf();
    const initialDelay = Math.max(1, expiresAtUtc - nowUtc);
    console.log("Initial scheduleRefreshToken Delay(ms)", initialDelay);
    const timerSource$ = Observable.timer(initialDelay);
    this.refreshTokenSubscription = timerSource$.subscribe(() => {
      this.refreshToken();      
    });
  }

  unscheduleRefreshToken() {
    if (this.refreshTokenSubscription) {
      this.refreshTokenSubscription.unsubscribe();
    }
  }
کار متد scheduleRefreshToken، شروع تایمر درخواست توکن جدید است.
- در ابتدا بررسی می‌کنیم که آیا کاربر لاگین کرده‌است یا خیر؟ آیا اصلا دارای توکنی هست یا خیر؟ اگر خیر، نیازی به شروع این تایمر نیست.
-  سپس تایمر قبلی را در صورت وجود، خاتمه می‌دهیم.
- در مرحله‌ی بعد، کار محاسبه‌ی میزان زمان تاخیر شروع تایمر Observable.timer را انجام می‌دهیم. پیشتر زمان انقضای توکن موجود را استخراج کرده‌ایم. اکنون این زمان را از زمان جاری سیستم برحسب UTC کسر می‌کنیم. مقدار حاصل، initialDelay این تایمر خواهد بود. یعنی این تایمر به مدت initialDelay صبر خواهد کرد و سپس تنها یکبار اجرا می‌شود. پس از اجرا، ابتدا متد refreshToken ذیل را فراخوانی می‌کند تا توکن جدیدی را دریافت کند.

در متد unscheduleRefreshToken کار لغو تایمر جاری در صورت وجود انجام می‌شود.

متد درخواست یک توکن جدید بر اساس refreshToken موجود نیز به صورت ذیل است:
  refreshToken() {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    const model = { refreshToken: this.getRawAuthToken(AuthTokenType.RefreshToken) };
    return this.http
      .post(`${this.appConfig.apiEndpoint}/${this.appConfig.refreshTokenPath}`, model, { headers: headers })
      .finally(() => {
        this.scheduleRefreshToken();
      })
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        console.log("RefreshToken Result", result);
        this.setLoginSession(result);
      });
  }
در اینجا هرزمانیکه تایمر اجرا شود، این متد فراخوانی شده و مقدار refreshToken فعلی را به سمت سرور ارسال می‌کند. سپس سرور این مقدار را بررسی کرده و در صورت تعیین اعتبار، یک access_token و refresh_token جدید را به سمت کلاینت ارسال می‌کند که نتیجه‌ی آن به متد setLoginSession جهت ذخیره سازی ارسال خواهد شد.
در آخر چون این تایمر، خود متوقف شونده‌است (متد Observable.timer بدون پارامتر دوم آن فراخوانی شده‌است)، یکبار دیگر کار زمانبندی دریافت توکن جدید بعدی را در متد finally انجام می‌دهیم (متد scheduleRefreshToken را مجددا فراخوانی می‌کنیم).


تغییرات مورد نیاز در سرویس Auth جهت زمانبندی دریافت توکن دسترسی جدید

تا اینجا متدهای مورد نیاز شروع زمانبندی دریافت توکن جدید، خاتمه‌ی زمانبندی و دریافت و به روز رسانی توکن جدید را پیاده سازی کردیم. محل قرارگیری و فراخوانی این متدها در سرویس Auth، به صورت ذیل هستند:
الف) در سازنده‌ی کلاس:
  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private browserStorageService: BrowserStorageService,
    private http: HttpClient,
    private router: Router
  ) {
    this.updateStatusOnPageRefresh();
    this.scheduleRefreshToken();
  }
این مورد برای مدیریت حالتی که کاربر صفحه را refresh کرده‌است و یا پس از مدتی مجددا از ابتدا برنامه را بارگذاری کرده‌است، مفید است.

ب) پس از لاگین موفقیت آمیز
در متد لاگین، پس از دریافت یک response موفقیت آمیز و تنظیم و ذخیره سازی توکن‌های دریافتی، کار زمانبندی دریافت توکن دسترسی بعدی بر اساس refresh_token فعلی انجام می‌شود:
this.setLoginSession(response);
this.scheduleRefreshToken();

ج) پس از خروج از سیستم
در متد logout، پس از حذف توکن‌های کاربر از کش مرورگر، کار لغو تایمر زمانبندی دریافت توکن بعدی نیز صورت خواهد گرفت:
this.deleteAuthTokens();
this.unscheduleRefreshToken();

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


ابتدا متد scheduleRefreshToken اجرا شده و تاخیر آغازین تایمر محاسبه شده‌است. پس از مدتی متد refreshToken توسط تایمر فراخوانی شده‌است. در آخر مجددا متد scheduleRefreshToken جهت شروع یک زمانبندی جدید، اجرا شده‌است.

اعداد initialDelay محاسبه شده‌ای را هم که ملاحظه می‌کنید، نزدیک به همان 2 دقیقه‌ی تنظیمات سمت سرور در فایل appsettings.json هستند:
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "http://localhost/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 2,
    "RefreshTokenExpirationMinutes": 60
  }


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
نظرات اشتراک‌ها
برای ساخت ویندوز 10 از چه زبان‌های برنامه نویسی استفاده شده‌است؟
The full source tree with all the code, test code and everything that together constitutes the “Windows source code” is more than a half terabyte in size, in more than 4 million files. You can spend a year (seriously) just drilling down the source tree, more than a half million folders containing the code for every component making up the OS workstation and server products and all their editions, tools, and associated developement kits, and see what’s in there, read the file names and try to figure out what does what. It would take a life (or two) to read it all. 
نظرات مطالب
نحوه‌ی مشاهده‌ی خروجی SQL تولید شده توسط WCF RIA Services
این مورد از چند دیدگاه قابل بررسی است:
- بحث lazy loading و eager loading : بارگذاری کردن یا نکردن اطلاعات وابسته به یک شیء تا زمانیکه به آن‌ها نیاز است یا اینکه هر آنچه نیاز است به یکباره load شود. با eager loading می‌شود تعداد رفت و برگشت‌ها به دیتابیس را در شرایطی که به آن نیاز است، به حداقل رساند.
- بحث دیگر Deferred Execution است (که با lazy loading متفاوت است) و اساس LINQ را تشکیل می‌دهد، فارغ از اینکه LINQ to SQL است یا LINQ to Entities یا ... . زمانیکه شما یک کوئری LINQ را می‌نویسید این کوئری بر روی دیتابیس اجرا نمی‌شود. این اجرا تا زمانیکه شما به اطلاعات آن نیازی نداشته باشید به تاخیر خواهد افتاد. به عبارتی اگر شما 20 عبارت LINQ را پشت سر هم تشکیل دهید حتی یک رفت و برگشت هم به دیتابیس نخواهید داشت. فقط expression tree آن تشکیل می‌شود. همینجا است که مفهوم وجودی SubmitChanges بهتر مشخص می‌شود. شما 10 عبارت تعریف رکوردهای جدید را با LINQ تشکیل دهید. هیچ اتفاقی بر روی دیتابیس رخ نخواهد داد (Deferred Execution). زمانیکه SubmitChanges فراخوانی می‌شود این expression tree نهایی به کدهای SQL متناظر ترجمه خواهد شد.
اطلاعات بیشتر : +
مطالب
بارگذاری پویای کامپوننت‌های Angular به همراه امکان Lazy loading پویای ماژول‌ها

در نسخه‌های قبل از Angular CLI 6.0، صرفا امکان Bundle کردن جداگانه‌ی ماژول‌هایی که در قسمت  loadChildren مرتبط با تنظیمات مسیریابی  ذکر شده بودند، وجود داشت. بنابراین در برخی از شرایط اگر نیاز به امکان بارگذاری ماژولی به صورت Lazy load بود، باید از سیستم مسیریابی استفاده می‌شد یا اینکه با یکسری ترفند، CLI و Webpack را مجبور به ساخت فایل chunk جداگانه برای ماژول مورد نظر می‌کردید. از زمان انتشار Angular CLI 6.0 امکان Lazy loading پویا نیز مهیا می‌باشد؛ به این ترتیب بدون وابستگی به سیستم مسیریابی، باز هم می‌توان از مزایای Lazy loading بهره برد. در این مطلب روش استفاده از این قابلیت و همچنین نحوه‌ی بارگذاری پویای یک کامپوننت مرتبط با یک ماژول Lazy load شده را بررسی خواهیم کرد. برای این منظور در ادامه با ایجاد یک TabLayout با استفاده از Angular Material Tabs با یکی از موارد پر استفاده‌ی قابلیت مذکور آشنا خواهیم شد.

پیش نیازها

کار را با طراحی و پیاده سازی TabService شروع می‌کنیم. برای این منظور یک سرویس را در فولدر services موجود در کنار CoreModule ایجاد خواهیم کرد؛ به این جهت ابتدا مدل‌های زیر را خواهیم داشت:

import { Type, ValueProvider } from '@angular/core';

export interface OpenNewTabModel {
  label: string;
  componentType: Type<any>;
  iconName: string;
  modulePath?: string;
  data?: ValueProvider[];
}
واسط تعریف شده‌ی در بالا به عنوان قرارداد مدل ورودی متد open مرتبط با سرویس TabService استفاده می‌شود. در اینجا componentType، نوع کامپوننت را مشخص می‌کند که قرار است داخل برگه‌ی جدید نمایش داده شود. modulePath هم به مسیر ماژولی که باید به صورت پویا بارگذاری شود، اشاره می‌کند. دلیل وجود خصوصیت data را نیز در ادامه خواهیم دید.
import { TabItemComponent } from './tab-item-component';

export interface TabItem {
  label: string;
  iconName: string;
  component: TabItemComponent;
}

OpenNewTabModel برای ارسال داده توسط مصرف کننده از این سرویس در نظر گرفته شده است. ولی واسط TabItem دارای خصوصیاتی می‌باشد که ما برای نمایش یک برگه‌ی جدید نیازمندیم. TabItemComponent نیز دارای خصوصیاتی است که مورد نیاز دایرکتیو« NgComponentOutlet» است. 

import { Injector, NgModuleFactory, Type } from '@angular/core';

export interface TabItemComponent {
  componentType: Type<any>;
  moduleFactory?: NgModuleFactory<any>;
  injector: Injector;
}

همانطور که اشاره شد، برای بارگذاری پویای یک کامپوننت از NgComponentOutlet استفاده خواهیم کرد؛ لذا اگر modulePath ای توسط مصرف کننده از TabService، مهیا شده باشد، لازم است ابتدا ماژول مورد نظر به صورت پویا بارگذاری شود و moduleFactory بدست آمده را به عنوان ورودی دایرکتیو مذکور ارسال کنیم. TabService، پیاده سازی به شکل زیر خواهد داشت:
import { BehaviorSubject, Observable } from 'rxjs';
import {
  Injectable,
  Injector,
  NgModuleFactory,
  NgModuleFactoryLoader
} from '@angular/core';

import { OpenNewTabModel } from '../models/open-new-tab-model';
import { TabItem } from '../models/tab-item';

@Injectable({
  providedIn: 'root'
})
export class TabService {
  private tabItemSubject: BehaviorSubject<TabItem[]> = new BehaviorSubject<
    TabItem[]
  >([]);

  constructor(
    private loader: NgModuleFactoryLoader,
    private injector: Injector
  ) {}

  get tabItems$(): Observable<TabItem[]> {
    return this.tabItemSubject.asObservable();
  }

  open(newTab: OpenNewTabModel) {
    if (newTab.modulePath) {
      this.loader
        .load(newTab.modulePath)
        .then((moduleFactory: NgModuleFactory<any>) => {
          this.openInternal(newTab, moduleFactory);
        });
    } else {
      this.openInternal(newTab);
    }
  }

  private openInternal(newTab: OpenNewTabModel, moduleFactory?: NgModuleFactory<any>) {
    const newTabItem: TabItem = {
      label: newTab.label,
      iconName: newTab.iconName,
      component: {
        componentType: newTab.componentType,
        moduleFactory: moduleFactory,
        injector: newTab.data
          ? Injector.create(newTab.data, this.injector)
          : this.injector
      }
    };

    this.tabItemSubject.getValue().push(newTabItem);
    this.tabItemSubject.next(this.tabItemSubject.getValue());
  }

  close(index: number) {
    this.tabItemSubject.getValue().splice(index, 1);
    this.tabItemSubject.next(this.tabItemSubject.getValue());
  }
}
روش کار به این شکل می‌باشد که یک مخزن، برای لیست برگه‌های درخواستی برای نمایش، تحت عنوان tabItemSubject و از نوع BehaviorSubject در نظر گرفته شده تا مصرف کننده از این سرویس که قصد نمایش برگه‌ها را دارد، از تغییرات لیست موجود آگاه شود. عموما TabsComponent، مشترک پراپرتی فقط خواندنی ‎‎‎tabItems‎$ خواهد شد و بقیه بخش‌ها صرفا دستور گشودن برگه‌ی جدید را با متد open صادر خواهند کرد.
یکی از وابستگی‌های این سرویس، وهله‌ای می‌باشد از کلاس  NgModuleFactoryLoader  که در سیستم مسیریابی نیز از همین کلاس برای بارگذاری ماژول‌ها استفاده می‌شود. البته نیاز است که یکی از پیاده سازی‌های این کلاس انتزاعی را به سیستم تزریق وابستگی‌ها نیز معرفی کنید:
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }
در بدنه متد open، ابتدا بررسی می‌شود که اگر modulePath مشخص شده‌است، ماژول مورد نظر ابتدا توسط متد load مرتبط با وهله NgModuleFactoryLoader به صورت پویا بارگذاری شود و سپس با استفاده از moduleFactory بدست آمده، متد openInternal فراخوانی خواهد شد.
 در بدنه متد openInternal، تنهای نکته‌ای که ذکر آن اهمیت دارد، مرتبط است به مقداردهی خصوصیت injector شیء ایجاد شده. باتوجه به اینکه تا زمان نگارش مطلب جاری امکان کار با Input‌ها و Output‌های کامپوننت مورد نظر که قرار است با استفاده از NgComponentOutlet بارگذاری شود، وجود ندارد، لذا راه حل فعلی، استفاده از سیستم تزریق وابستگی‌ها می‌باشد. برای این منظور، با استفاده از متد استاتیک create کلاس Injector یک child injector ایجاد شده و ValueProvider‌های مشخص شده توسط خصوصیت data، به صورت خودکار رجیستر خواهند شد. در نهایت آگاه سازی مشترکین خصوصیت ‎‎‎tabItems‎با استفاده از فراخوانی متد next مرتبط با tabItemSubject انجام می‌گیرد.

پیاده سازی TabsComponent
import { Component, OnInit } from '@angular/core';

import { TabService } from './../../../services/tab.service';

@Component({
  selector: 'app-tabs',
  templateUrl: './tabs.component.html',
  styleUrls: ['./tabs.component.scss']
})
export class TabsComponent implements OnInit {
  constructor(public service: TabService) {}

  ngOnInit() {}
}

همانطور که عنوان شد، مشترک اصلی خصوصیت tabItems سرویس TabService، کامپوننت تعریف شده‌ی بالا می‌باشد. قالب مرتبط با آن به شکل زیر است:
<mat-tab-group>
  <mat-tab
    *ngFor="let tabItem of (service.tabItems$ | async); let index = index"
  >
    <ng-template mat-tab-label>
      <mat-icon
        class="icon"
        aria-label="icon for tab"
      >{{tabItem.iconName}}</mat-icon>
      <span class="full">{{ tabItem.label }}</span>
    
      <mat-icon
        class="close"
        (click)="service.close(index)"
        aria-label="close tab button"
        >close</mat-icon
      >
      <!-- </button> -->
    </ng-template>

    <ng-container *ngIf="tabItem.component.moduleFactory">
      <ng-container
        *ngComponentOutlet="
          tabItem.component.componentType;
          ngModuleFactory: tabItem.component.moduleFactory;
          injector: tabItem.component.injector
        "
      >
      </ng-container>
    </ng-container>
    <ng-container *ngIf="!tabItem.component.moduleFactory">
      <ng-container
        *ngComponentOutlet="
          tabItem.component.componentType;
          injector: tabItem.component.injector
        "
      >
      </ng-container>
    </ng-container>
  </mat-tab>
</mat-tab-group>

در تکه کد بالا، ابتدا با استفاده از وهله تزریق شده TabService در کامپوننت مذکور، به شکل زیر، مشترک تغییرات لیست برگه‌ها شده‌ایم و با استفاده از دایرکتیو ‎*ngFor به ازای تک تک tabItem‌های درخواست شده برای گشوده شدن، به شکل زیر کار وهله سازی پویا از کامپوننت مشخص شده انجام می‌شود:

<ng-container *ngComponentOutlet="tabItem.component.componentType; ngModuleFactory: tabItem.component.moduleFactory; injector: tabItem.component.injector">
</ng-container>

خوب، با استفاده از آنچه تا اینجای مطلب بررسی شد، می‌توان یک سیستم راهبری مبتنی بر Tab را نیز برپا کرد که مطلب جدایی را می‌طلبد. برای تکمیل مکانیزم بارگذاری پویای ماژول‌ها، نیاز است تا مسیر ماژول مورد نظر را در فایل angular.json و بخش lazyModules به شکل زیر معرفی کنید:

"build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/MaterialAngularTabLayout",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
              "src/styles.scss"
            ],
            "lazyModules": [
              "src/app/lazy/lazy.module"
            ],
            "scripts": []
          },

به عنوان مثال قصد داریم ماژول LazyModule را به صورت پویا بارگذاری کرده و LazyComponent موجود در این ماژول را به صورت پویا در برگه‌ی جدیدی نمایش دهیم. برای این منظور کدهای فایل AppComponent.ts را به شکل زیر تغییر خواهیم داد:

import { Component } from '@angular/core';
import { IdModel } from './core/models/id-model';
import { LazyComponent } from './lazy/lazy.component';
import { OpenNewTabModel } from './core/models/open-new-tab-model';
import { TabService } from './core/services/tab.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'MaterialAngularTabLayout';
  constructor(private tabService: TabService) {}
  loadLazyComponent() {
    this.tabService.open(<OpenNewTabModel>{
      label: 'Loaded Lazy Component',
      iconName: 'thumb_up',
      componentType: LazyComponent,
      modulePath: 'src/app/lazy/lazy.module#LazyModule',
      data: [{ provide: IdModel, useValue: <IdModel>{ id: 1 } }]
    });
  }
}

در تکه کد بالا با تزریق TabService به سازنده‌ی آن، قصد گشودن برگه‌ی جدیدی را توسط متد open آن، داریم. در بدنه‌ی متد loadLazyComponent یک شیء با قرارداد OpenNewTabModel ایجاد شده و به عنوان آرگومان به متد open ارسال شده است. توجه داشته باشید که modulePath اینجا نیز به مانند خصوصیت loadChildren مرتبط با اشیاء مسیریابی، باید مقدار دهی شود. همچنین قصد داشتیم اطلاعاتی را نیز به کامپوننت مورد نظر ارسال کنیم؛ همانند مکانیزم مسیریابی که با پارامترها این چنین کارهایی صورت می‌پذیرد. در اینجا از یک کلاس به شکل زیر استفاده شده‌است:

export class IdModel {
  constructor(public id: number) {}
}

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

import { Component, OnInit } from '@angular/core';

import { IdModel } from './../core/models/id-model';

@Component({
  selector: 'app-lazy',
  templateUrl: './lazy.component.html',
  styleUrls: ['./lazy.component.scss']
})
export class LazyComponent implements OnInit {
  constructor(private model: IdModel) {}

  ngOnInit() {
    console.log(this.model);
  }
}

البته فراموش نکنید کامپوننتی را که نیاز است به صورت پویا بارگذاری شود، در قسمت entryComponents مرتبط با NgModule متناظر به شکل نیز معرفی کنید:

import { CommonModule } from '@angular/common';
import { LazyComponent } from './lazy.component';
import { NgModule } from '@angular/core';

@NgModule({
  imports: [CommonModule],
  declarations: [LazyComponent],
  entryComponents: [LazyComponent]
})
export class LazyModule {}

با خروجی زیر:

و chunk تولید شده برای ماژول مورد نظر:


در صورتیکه در حالت production پروژه را بیلد کنید، هش مرتبط برای chunk تولید شده نیز ایجاد خواهد شد.


کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید.