مطالب
استفاده از افزونه‌ی jsTree در ASP.NET MVC
jsTree  یکی از افزونه‌های بسیار محبوب jQuery جهت نمایش ساختارهای سلسله مراتبی، خود ارجاع دهنده و تو در تو است. روش ابتدایی استفاده از آن تعریف یک سری ul و li ثابت در صفحه و سپس فراخوانی این افزونه بر روی آن‌ها است که سبب نمایش درخت‌واره‌ا‌ی این اطلاعات خواهد شد. روش پیشرفته‌تر آن به همراه کار با داده‌های JSON و دریافت پویای اطلاعات از سرور است که در ادامه به بررسی آن خواهیم پرداخت.


دریافت افزونه‌ی jsTree

برای دریافت افزونه‌ی jsTree می‌توان به مخزن کد آن در Github مراجعه کرد و همچنین مستندات آن‌را در سایت jstree.com قابل مطالعه هستند.


تنظیمات مقدماتی jsTree

در این مطلب فرض شده‌است که فایل jstree.min.js، در پوشه‌ی Scripts و فایل‌های CSS آن در پوشه‌ی Content\themes\default کپی شده‌اند.
به این ترتیب layout برنامه چنین شکلی را خواهد یافت:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    
    <link href="~/Content/Site.css" rel="stylesheet" />
    <link href="~/Content/themes/default/style.min.css" rel="stylesheet" />
    <script src="~/Scripts/jquery.min.js"></script>
    <script src="~/Scripts/jstree.min.js"></script>
</head>
<body dir="rtl">
    @RenderBody()
    
    @RenderSection("scripts", required: false)
</body>
</html>

نمایش راست به چپ اطلاعات

در کدهای این افزونه به تگ body و ویژگی dir آن برای تشخیص راست به چپ بودن محیط دقت می‌شود. به همین جهت این تعریف را در layout فوق ملاحظه می‌کنید. برای مثال اگر به فایل jstree.contextmenu.js (موجود در مجموعه سورس‌های این افزونه) مراجعه کنید، یک چنین تعریفی قابل مشاهده است:
 right_to_left = $("body").css("direction") === "rtl";

تهیه ساختاری جهت ارائه‌ی خروجی JSON

با توجه به اینکه قصد داریم به صورت پویا با این افزونه کار کنیم، نیاز است بتوانیم ساختار سلسله مراتبی مدنظر را با فرمت JSON ارائه دهیم. در ادامه کلاس‌هایی که معادل فرمت JSON قابل قبول توسط این افزونه را تولید می‌کنند، ملاحظه می‌کنید:
using System.Collections.Generic;

namespace MvcJSTree.Models
{
    public class JsTreeNode
    {
        public string id { set; get; } // نام این خواص باید با مستندات هماهنگ باشد
        public string text { set; get; }
        public string icon { set; get; }
        public JsTreeNodeState state { set; get; }
        public List<JsTreeNode> children { set; get; }
        public JsTreeNodeLiAttributes li_attr { set; get; }
        public JsTreeNodeAAttributes a_attr { set; get; }

        public JsTreeNode()
        {
            state = new JsTreeNodeState();
            children = new List<JsTreeNode>();
            li_attr = new JsTreeNodeLiAttributes();
            a_attr = new JsTreeNodeAAttributes();
        }
    }

    public class JsTreeNodeAAttributes
    {
        // به هر تعداد و نام اختیاری می‌توان خاصیت تعریف کرد
        public string href { set; get; }
    }

    public class JsTreeNodeLiAttributes
    {
        // به هر تعداد و نام اختیاری می‌توان خاصیت تعریف کرد
        public string data { set; get; }
    }

    public class JsTreeNodeState
    {
        public bool opened { set; get; }
        public bool disabled { set; get; }
        public bool selected { set; get; }

        public JsTreeNodeState()
        {
            opened = true;
        }
    }
}
در اینجا به چند نکته باید دقت داشت:
- هر چند اسامی مانند a_attr، مطابق اصول نامگذاری دات نت نیستند، ولی این نام‌ها را تغییر ندهید. زیرا این افزونه دقیقا به همین نام‌ها و با همین املاء نیاز دارد.
- id، می‌تواند دقیقا معادل id یک رکورد در بانک اطلاعاتی باشد. Text عنوان گره‌ای (node) است که نمایش داده می‌شود. icon در اینجا مسیر یک فایل png است جهت نمایش در کنار عنوان هر گره. توسط state می‌توان مشخص کرد که زیر شاخه‌ی جاری به صورت باز نمایش داده شود یا بسته. به کمک خاصیت children می‌توان زیر شاخه‌ها را تا هر سطح و تعدادی که نیاز است تعریف نمود.
- خاصیت‌های li_attr و a_attr کاملا دلخواه هستند. برای مثال در اینجا دو خاصیت href و data را در کلاس‌های مرتبط با آن‌ها مشاهده می‌کنید. می‌توانید در اینجا به هر تعداد ویژگی سفارشی دیگری که جهت تعریف یک گره نیاز است، خاصیت اضافه کنید.

ساده‌ترین مثالی که از ساختار فوق می‌تواند استفاده کند، اکشن متد زیر است:
        [HttpPost]
        public ActionResult GetTreeJson()
        {
            var nodesList = new List<JsTreeNode>();

            var rootNode = new JsTreeNode
            {
                id = "dir",
                text = "Root 1",
                icon = Url.Content("~/Content/images/tree_icon.png"),
                a_attr = { href = "http://www.bing.com" }
            };
            nodesList.Add(rootNode);

            nodesList.Add(new JsTreeNode
            {
                id = "test1",
                text = "Root 2",
                icon = Url.Content("~/Content/images/tree_icon.png"),
                a_attr = { href = "http://www.bing.com" }
            });

            return Json(nodesList, JsonRequestBehavior.AllowGet);
        }
در ابتدا لیست گره‌ها تعریف می‌شود و سپس برای نمونه در این مثال، دو گره تعریف شده‌اند و در ادامه با فرمت JSON در اختیار افزونه قرار گرفته‌اند.
بنابراین ساختارهای خود ارجاع دهنده‌ را به خوبی می‌توان با این افزونه وفق داد.


فعال سازی اولیه سمت کلاینت افزونه jsTree

برای استفاد‌ه‌ی پویای از این افزونه در سمت کلاینت، فقط نیاز به یک DIV خالی است:
 <div id="jstree">
</div>
سپس jstree را بر روی این DIV فراخوانی می‌کنیم:
            $('#jstree').jstree({
                "core": {
                    "multiple": false,
                    "check_callback": true,
                    'data': {
                        'url': '@getTreeJsonUrl',
                        "type": "POST",
                        "dataType": "json",
                        "contentType": "application/json; charset=utf8",
                        'data': function (node) {
                            return { 'id': node.id };
                        }
                    },
                    'themes': {
                        'variant': 'small',
                        'stripes': true
                    }
                },
                "types": {
                    "default": {
                        "icon": '@Url.Content("~/Content/images/bookmark_book_open.png")'
                    },
                },
                "plugins": ["contextmenu", "dnd", "state", "types", "wholerow", "sort", "unique"],
                "contextmenu": {
                    "items": function (o, cb) {
                        var items = $.jstree.defaults.contextmenu.items();
                        items["create"].label = "ایجاد زیر شاخه";
                        items["rename"].label = "تغییر نام";
                        items["remove"].label = "حذف";
                        var cpp = items["ccp"];
                        cpp.label = "ویرایش";
                        var subMenu = cpp["submenu"];
                        subMenu["copy"].label = "کپی";
                        subMenu["paste"].label = "پیست";
                        subMenu["cut"].label = "برش";
                        return items;
                    }
                }
            });
توضیحات

- multiple : false به این معنا است که نمی‌خواهیم کاربر بتواند چندین گره را با نگه داشتن دکمه‌ی کنترل انتخاب کند.
- check_callback : true کدهای مرتبط با منوی کلیک سمت راست ماوس را فعال می‌کند.
- در قسمت data کار تبادل اطلاعات با سرور جهت دریافت فرمت JSON ایی که به آن اشاره شد، انجام می‌شود. متغیر getTreeJsonUrl یک چنین شکلی را می‌تواند داشته باشد:
 @{
ViewBag.Title = "Demo";
var getTreeJsonUrl =  Url.Action(actionName: "GetTreeJson", controllerName: "Home");
}
- در قسمت themes مشخص کرده‌ایم که از قالب small آن به همراه نمایش یک درمیان پس زمینه‌ی روشن و خاکستری استفاده شود. قالب large نیز دارد.
- در قسمت types که مرتبط است با افزونه‌ای به همین نام، آیکن پیش فرض یک نود جدید ایجاد شده را مشخص کرده‌ایم.
- گزینه‌ی plugins، لیست افزونه‌های اختیاری این افزونه را مشخص می‌کند. برای مثال contextmenu منوی کلیک سمت راست ماوس را فعال می‌کند، dnd همان کشیدن و رها کردن گره‌ها است در زیر شاخه‌های مختلف. افزونه‌ی state، انتخاب جاری کاربر را در سمت کلاینت ذخیره و در مراجعه‌ی بعدی او بازیابی می‌کند. با ذکر افزونه‌ی wholerow سبب می‌شویم که انتخاب یک گره، معادل انتخاب یک ردیف کامل از صفحه باشد. افزونه‌ی sort کار مرتب سازی خودکار اعضای یک زیر شاخه را انجام می‌دهد. افزونه‌ی unique سبب می‌شود تا در یک زیر شاخه نتوان دو عنوان یکسان را تعریف کرد.
- در قسمت contextmenu نحوه‌ی بومی سازی گزینه‌های منوی کلیک راست ماوس را مشاهده می‌کنید. در حالت پیش فرض، عناوینی مانند create، rename و امثال آن نمایش داده می‌شوند که به نحو فوق می‌توان آن‌را تغییر داد.

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


ذخیره سازی گره‌های جدید و تغییرات سلسله مراتب پویای تعریف شده در سمت سرور

همانطور که عنوان شد، اگر افزونه‌ی اختیاری contextmenu را فعال کنیم، امکان افزودن، ویرایش و حذف گره‌ها و زیر شاخه‌ها را خواهیم یافت. برای انتقال این تغییرات به سمت سرور، باید به نحو ذیل عمل کرد:
            $('#jstree').jstree({
              // تمام تنظیمات مانند قبل
            }).on('delete_node.jstree', function (e, data) {
                })
                .on('create_node.jstree', function (e, data) {
                })
                .on('rename_node.jstree', function (e, data) {
                })
                .on('move_node.jstree', function (e, data) {
                })
                .on('copy_node.jstree', function (e, data) {
                })
                .on('changed.jstree', function (e, data) {
                })
                .on('dblclick.jstree', function (e) {
                })
                .on('select_node.jstree', function (e, data) {
                });
در اینجا نحوه‌ی تحت کنترل قرار دادن رخ‌دادهای مختلف این افزونه را مشاهده می‌کنید. برای مثال در callback مرتبط با delete_node کار حذف یک گره اطلاع رسانی می‌شود. create_node مربوط است به ایجاد یک گره یا زیر شاخه‌ی جدید. rename_node پس از تغییر نام یک گره فراخوانی خواهد شد. move_node مربوط است به کشیدن و رها کردن یک گره در یک زیر شاخه‌ی دیگر. copy_node برای copy/paste یک گره تعریف شده‌است. Changed یک callback عمومی است. dblclick برای عکس العمل نشان دادن به رخ‌داد دوبار کلیک کردن بر روی یک گره می‌تواند بکار گرفته شود. select_node با انتخاب یک گره فعال می‌شود.
در تمام این حالات، جایی که data در اختیار ما است، می‌توان یک چنین ساختار جاوا اسکریپتی را برای ارسال به سرور طراحی کرد:
        function postJsTreeOperation(operation, data, onDone, onFail) {
            $.post('@doJsTreeOperationUrl',
                {
                    'operation': operation,
                    'id': data.node.id,
                    'parentId': data.node.parent,
                    'position': data.position,
                    'text': data.node.text,
                    'originalId': data.original ? data.original.id : data.node.original.id,
                    'href': data.node.a_attr.href
                })
                .done(function (result) {
                    onDone(result);
                })
                .fail(function (result) {
                    alert('failed.....');
                    onFail(result);
                });
        }
به این ترتیب در سمت سرور می‌توان id یک گره، متن تغییر یافته آن، والد گره و بسیاری از مشخصات دیگر را دریافت و ثبت کرد. پس از تعریف متد postJsTreeOperation فوق، آن‌را باید به callbackهایی که پیشتر معرفی شدند، اضافه کرد؛ برای مثال:
                .on('create_node.jstree', function (e, data) {
                    postJsTreeOperation('CreateNode', data,
                        function (result) {
                            data.instance.set_id(data.node, result.id);
                        },
                        function (result) {
                            data.instance.refresh();
                        });
                })
در اینجا متد postJsTreeOperation، یک Operation خاص را مانند CreateNode (تعریف شده در enum ایی به نام JsTreeOperation در سمت سرور) به همراه data، به سرور post می‌کند.
و معادل سمت سرور دریافت کننده‌ی این اطلاعات، اکشن متد ذیل می‌تواند باشد:
        [HttpPost]
        public ActionResult DoJsTreeOperation(JsTreeOperationData data)
        {
            switch (data.Operation)
            {
                case JsTreeOperation.CopyNode:
                case JsTreeOperation.CreateNode:
                    //todo: save data
                    var rnd = new Random(); // آی دی رکورد پس از ثبت در بانک اطلاعاتی دریافت و بازگشت داده شود
                    return Json(new { id = rnd.Next() }, JsonRequestBehavior.AllowGet);

                case JsTreeOperation.DeleteNode:
                    //todo: save data
                    return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);

                case JsTreeOperation.MoveNode:
                    //todo: save data
                    return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);

                case JsTreeOperation.RenameNode:
                    //todo: save data
                    return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);

                default:
                    throw new InvalidOperationException(string.Format("{0} is not supported.", data.Operation));
            }
        }
که در آن ساختار JsTreeOperationData به نحو ذیل تعریف شده‌است:
namespace MvcJSTree.Models
{
    public enum JsTreeOperation
    {
        DeleteNode,
        CreateNode,
        RenameNode,
        MoveNode,
        CopyNode
    }

    public class JsTreeOperationData
    {
        public JsTreeOperation Operation { set; get; }
        public string Id { set; get; }
        public string ParentId { set; get; }
        public string OriginalId { set; get; }
        public string Text { set; get; }
        public string Position { set; get; }
        public string Href { set; get; }
    }
}
این ساختار دقیقا با اعضای شیء جاوا اسکریپتی که متد postJsTreeOperation به سمت سرور ارسال می‌کند، تطابق دارد.
در اینجا Href را نیز مشاهده می‌کنید. همانطور که عنوان شد، اعضای JsTreeNodeAAttributes اختیاری هستند. بنابراین اگر این اعضاء را تغییر دادید، باید خواص JsTreeOperationData و همچنین اعضای شیء تعریف شده در postJsTreeOperation را نیز تغییر دهید تا با هم تطابق پیدا کنند.


چند نکته‌ی تکمیلی

اگر می‌خواهید که با دوبار کلیک بر روی یک گره، کاربر به href آن هدایت شود، می‌توان از کد ذیل استفاده کرد:
               var selectedData;
               // ...
                .on('dblclick.jstree', function (e) {
                    var href = selectedData.node.a_attr.href;
                    alert('selected node: ' + selectedData.node.text + ', href:' + href);

                    // auto redirect
                    if (href) {
                        window.location = href;
                    }

                    // activate edit mode
                    //var inst = $.jstree.reference(selectedData.node);
                    //inst.edit(selectedData.node);
                })
                .on('select_node.jstree', function (e, data) {
                    //alert('selected node: ' + data.node.text);
                    selectedData = data;
                });
در callback مرتبط با select_node می‌توان به گره انتخابی درستی یافت. سپس می‌توان این گره را در callback متناظر با dblclick برای یافتن href و مقدار دهی window.location که معادل redirect سمت کاربر است، بکار برد.
حتی اگر خواستید که با دوبار کلیک بر روی یک گره، گزینه‌ی ویرایش آن فعال شود، کدهای آن را به صورت کامنت مشاهده می‌کنید.


مثال کامل این بحث را از اینجا می‌توانید دریافت کنید:
MvcJSTree.zip
مطالب دوره‌ها
طراحی روابط و ارجاعات در RavenDB
در قسمت‌های قبل، با پیش زمینه‌ی ذهنی طراحی مدل‌های RavenDB به همراه اصول مقدماتی کوئری نویسی آن آشنا شدیم. در این قسمت قصد داریم معادل‌های روابط موجود در بانک‌های اطلاعاتی رابطه‌ای را در RavenDB و مطابق ذهنیت غیر رابطه‌ای آن، مدلسازی کنیم و مثال‌های بیشتری را بررسی نمائیم.

مدیریت روابط در RavenDB

یکی از اصول طراحی مدل‌ها در RavenDB، مستقل بودن اسناد یا documents است. به این ترتیب کلیه اطلاعاتی که یک سند نیاز دارد، داخل همان سند ذخیره می‌شوند (به این نوع شیء،  Root Aggregate هم گفته می‌شود). اما این اصل سبب نخواهد شد تا نتوان یا نباید ارتباطی را بین اسناد تعریف کرد. بنابراین سؤال مهم اینجا است که چه اطلاعات مرتبطی باید داخل یک سند ذخیره شوند و چه اطلاعاتی باید به سند دیگری ارجاع داده شوند. برای پاسخ به این سؤال سه روش ذیل را باید مدنظر داشت:

الف) Denormalized references
فرض کنید در دنیای رابطه‌ای دو جدول سفارش و مشتری را دارید. در این حالت، جدول سفارش تنها شماره آی دی اطلاعات مشتری را از جدول مشتری یا کاربران سیستم، در خود ذخیره خواهد کرد. به این ترتیب از تکرار اطلاعات مشتری در جدول سفارشات جلوگیری می‌گردد. اما اگر اطلاعات پرکاربرد مشتری را در داخل جدول سفارش قرار دهیم به آن denormalized reference گفته می‌شود.
ایجاد denormalized reference یکی از روش‌های مرسوم در دنیای NoSQL و RavenDB است؛ خصوصا جهت سهولت نمایش اطلاعات. به این ترتیب ارجاع به سندهای دیگر کمتر شده و ترافیک شبکه نیز کاهش می‌یابد. برای مثال در اینجا نام و آدرس مشتری را داخل سند ثبت شده قرار می‌دهیم و از سایر اطلاعات او (که اهمیت نمایشی ندارند) مانند کلمه عبور و امثال آن صرفنظر خواهیم کرد.
اینجا است که یک سری از سؤالات مطرح خواهند شد مانند : «اگر آدرس مشتری تغییر کرد، چطور؟»
بنابراین بهترین حالت استفاده از روش denormalized references محدود خواهد شد به موارد ذیل:
الف) قید اطلاعاتی که به ندرت تغییر می‌کنند. برای مثال نام یک شخص یا نام یک کشور، استان یا شهر.
ب) ثبت اطلاعات تکراری که در طول زمان تغییر می‌کنند، اما باید تاریخچه‌ی آن‌ها حفظ شوند. برای مثال اگر آدرس مشتری تغییر کرده است، واقعا اجناس سندهای قبلی او، صرفنظر از آدرس جدیدی که اعلام کرده است، به آدرس قبلی او ارسال شده‌اند و این تاریخچه باید در سیستم حفظ شوند.
ج) اطلاعاتی که ممکن است بعدها حذف شوند؛ اما نیاز است سابقه اسناد قبلی تخریب نشوند. برای مثال کارخانه‌ای را درنظر بگیرید که امسال یک سری چینی خاص را تولید می‌کند و می‌فروشد. سال بعد خط تولید خود را عوض کرده و سری اجناس دیگری را شروع به تولید و فروش خواهد کرد. در بانک‌های اطلاعاتی رابطه‌ای نمی‌توان اجناسی را که در جداول دیگر ارجاع دارند، به این سادگی‌ها حذف کرد. در اینجا باید از روش‌هایی مانند تعریف فیلد بیتی IsDeleted برای مخفی کردن ظاهری رکوردهای موجود کمک گرفت. اما در دنیای رابطه‌ای، اطلاعات مهم محصول را در سند اصلی ثبت کنید. بعد هر زمانیکه نیازی به محصول نبود، کلا تعریف آن‌را حذف نمائید.


ب) Includes
Includes در RavenDB برای پوشش مشکلات denormalization ارائه شده است. در اینجا بجای اینکه یک شیء کپی اطلاعات پرکاربرد شیء‌ایی دیگر را در خود ذخیره کند، تنها ارجاعی (یک Id رشته‌ای) از آن شیء را در سند مرتبط ذخیره خواهد کرد.
public class Order
{
    public string CustomerId { get; set; }
    public LineItem[] LineItems { get; set; }
    public double TotalPrice { get; set; }
}
 
public class Customer
{
    public string Name { get; set; }
    public string Address { get; set; }
    public short Age { get; set; }
    public string HashedPassword { get; set; }
}
برای نمونه در کلاس Order شاهد یک Id رشته‌ای ارجاع دهنده به کلاس Customer هستیم. هرگاه که نیاز به بارگذاری اطلاعات شیء Order به همراه کل اطلاعات مشتری او تنها در یک رفت و برگشت به بانک اطلاعاتی باشد، می‌توان از متد الحاقی Include مختص RavenDB استفاده کرد:
var order = session.Include<Order>(x => x.CustomerId)
                   .Load("orders/1234");
 
// این کوئری از کش سشن خوانده می‌شود و کاری به سرور ندارد
var cust = session.Load<Customer>(order.CustomerId);
همانطور که مشاهده می‌کنید، با ذکر متد Include، اعلام کرده‌ایم که مایل هستیم تا اطلاعات سند مشتری متناظر را نیز داشته باشیم. در این حالت در Load بعدی که بر اساس Id مشتری انجام شده، دیگر رفت و برگشتی به سرور انجام نشده و اطلاعات مشتری از کش سشن جاری که پیشتر با فراخوانی Include مقدار دهی شده است، دریافت می‌گردد.
حتی می‌توان چند سند مرتبط را با هم بارگذاری کرد؛ با حداقل رفت و برگشت به سرور:
var orders = session.Include<Order>(x => x.CustomerId)
    .Load("orders/1234", "orders/4321");
 
foreach (var order in orders)
{
    // این کوئری‌ها سمت کلاینت هستند و به سرور ارسال نمی‌شوند
    var cust = session.Load<Customer>(order.CustomerId);
}
همچنین امکان استفاده از متد Include در LINQ API نیز پیش بینی شده است. برای این منظور باید از متد Customize استفاده کرد:
var orders = session.Query<Order>()
    .Customize(x => x.Include<Order>(o => o.CustomerId))
    .Where(x => x.TotalPrice > 100)
    .ToList();
 
foreach (var order in orders)
{
    // این کوئری‌ها سمت کلاینت اجرا می‌شوند
    var cust = session.Load<Customer>(order.CustomerId);
}


Includeهای یک به چند

اکنون فرض کنید به کلاس سفارش، آرایه تامین کننده‌ها نیز افزوده شده است (رابطه یک به چند):
public class Order
{
    public string CustomerId { get; set; }
    public string[] SupplierIds { get; set; }
    public LineItem[] LineItems { get; set; }
    public double TotalPrice { get; set; }
}
بارگذاری یکباره روابط یک به چند نیز با Include میسر است:
var orders = session.Include<Order>(x => x.SupplierIds)
    .Load("orders/1234", "orders/4321");
 
foreach (var order in orders)
{
    foreach (var supplierId in order.SupplierIds)
    {
        // از کش سشن خوانده می‌شود
        var supp = session.Load<Supplier>(supplierId);
    }
}



Includeهای چند سطحی

در اینجا کلاس سفارشی را در نظر بگیرید که دارای خاصیت ارجاع دهنده نیز هست. این خاصیت به شکل یک کلاس تعریف شده است و نه به شکل  یک آی دی رشته‌ای:
public class Order
{
    public string CustomerId { get; set; }
    public string[] SupplierIds { get; set; }
    public Referral Refferal { get; set; }
    public LineItem[] LineItems { get; set; }
    public double TotalPrice { get; set; }
}

public class Referral
{
    public string CustomerId { get; set; }
    public double CommissionPercentage { get; set; }
}
متد Include امکان ارجاع به خواص تو در تو را نیز دارد:
var order = session.Include<Order>(x => x.Refferal.CustomerId)
    .Load("orders/1234");
 
// از کش سشن خوانده می‌شود
var referrer = session.Load<Customer>(order.Refferal.CustomerId);
همچنین این متد با مجموعه‌ها نیز کار می‌کند. برای مثال اگر تعریف متد LineItem به صورت زیر باشد:
public class LineItem
{
    public string ProductId { get; set; }
    public string Name { get; set; }
    public int Quantity { get; set; }
    public double Price { get; set; }
}
برای بارگذاری یکباره اسناد مرتبط می‌توان به روش ذیل عمل کرد:
var order = session.Include<Order>(x => x.LineItems.Select(li => li.ProductId))
    .Load("orders/1234");
 
foreach (var lineItem in order.LineItems)
{
    // از کش سمت کلاینت خوانده می‌شود
    var product = session.Load<Product>(lineItem.ProductId);
}

و به صورت خلاصه برای باگذاری اسناد مرتبط، دیگر از دو کوئری پشت سر هم ذیل استفاده نکنید:
var order = session.Load<Order>("orders/1");
var customer = session.Load<Customer>(order.CustomerId);
این دو کوئری یعنی دوبار رفت و برگشت به سرور. با استفاده از Include می‌توان تعداد رفت و برگشت‌ها و همچنین ترافیک شبکه را کاهش داد. به علاوه سرعت کار نیز افزایش خواهد یافت.


ج) تفاوت بین Reference و Relationship

برای درک اینکه آیا اطلاعات یک شیء مرتبط را بهتر است داخل شیء اصلی (Aggregate rooe) ذخیره کرد یا خیر، باید مفاهیم ارجاع و ارتباط را بررسی کنیم.
اگر به مثال سفارش و مشتری دقت کنیم، یک سفارش را بدون مشتری نیز می‌توان تکمیل کرد. برای مثال بسیاری از فروشگاه‌ها به همین نحو عمل می‌کنند و اگر شماره Id مشتری را به سندی اضافه می‌کنیم، صرفا جهت این است که بدانیم این سند متعلق به شخص دیگری نیست. بنابراین «ارجاعی» به کاربر در جدول سفارش می‌تواند وجود داشته باشد.
اکنون اقلام سفارش را درنظر بگیرید. هر آیتم سفارش تنها با بودن آن سفارش خاص است که معنا پیدا می‌کنند و نه بدون آن. این آیتم می‌تواند ارجاعی به محصول مرتبط داشته باشد. اینجا است که می‌گوییم اقلام سند با سفارش «در ارتباط» هستند؛ اما یک سند ارجاعی دارد به مشتری.
از این دو مفهوم برای تشخیص تشکیل Root Aggregate استفاده می‌شود. به این ترتیب تشخیص داده‌ایم اقلام سند، Root Aggregate را تشکیل می‌دهند؛ بنابراین ذخیره سازی تمام آن‌ها داخل یک سند RavenDB معنا پیدا می‌کند.


چند مثال برای درک بهتر نحوه طراحی اسناد در RavenDB

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

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

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

بنابراین در حین طراحی اسناد NoSQL باید به اعمال و «محدوده‌های تراکنشی» انجام شده دقت داشت تا اینکه صرفا عنوان شود این یک رابطه یک به چند یا چند به چند است.
نظرات مطالب
نمایش فرم‌های مودال Ajax ایی در ASP.NET MVC به کمک Twitter Bootstrap
ممنون از شما واقعاً مطلب جالبی بود،
فقط یک سوال :
کلاس‌های مدل من Product و Category است، نحوه نمایش گروه محصولات در DropDownList به چه صورتی است؟
ابتدا من یک ViewModel ایجاد کردم :
public class ProductCategoryViewModel
{
public Product Product{get;set;}
public Category Category{get;set;}
}
و سپس در RenderModalPartialView به PartialView مربوطه پاس دادم :

public ActionResult RenderModalPartialView()
        {
            //رندر پارشال ویوو صفحه مودال به همراه اطلاعات مورد نیاز آن
            return PartialView(viewName: "_ModalPartialView", model: new ProductCategoryViewModel());
        }
در PartialView هم برای نمایش نام گروه محصولات از طریق ViewBag لیست گروه محصولات داده شده را در یک DropDownList به صورت زیر استفاده کردم :
@Html.DropDownListFor(model => model.Product.CategoryId, (SelectList)ViewBag.CategryNames, "--گروه موردنظر را انتخاب کنید--")
اما روش فوق کارساز نیست و با کلیک بر روی باتن Modal  را نمایش نمی‌دهد .

و یه سوال دیگه اینکه چطور قالب T4 تولید شده رو برای حالت فرم‌های مدال بازنویسی کنیم؟ البته ظاهراً باید توی قسمت :
if(mvcHost.IsPartialView) {
#>

<#

قالب موردنظر (_ModalPartialView ) برای حالت مدال فرم رو باید بنویسم، من با سینتکس T4 آشنا نیستم ، میشه لطف کنید راهنمایی کنید؟
نظرات مطالب
استخراج اطلاعات از صفحات وب با کمک HtmlAgilityPack
پیشنهاد من این است که یک دوره سی شارپ مقدماتی رو بگذرونید. با مباحثی مانند نحوه تعریف فضای نام و روش فراخوانی یک متد استاتیک از کلاس متناظر با آن آشنا شوید.
یک دوره خوب مقدماتی سی‌شارپ
مطالب دوره‌ها
استفاده از StructureMap به عنوان یک IoC Container
StructureMap یکی از IoC containerهای بسیار غنی سورس باز نوشته شده برای دات نت فریم ورک است. امکان تنظیمات آن توسط کدنویسی و یا همان Fluent interfaces، به کمک فایل‌های کانفیگ XML و همچنین استفاده از ویژگی‌ها یا Attributes نیز میسر است. امکانات جانبی دیگری را نیز مانند یکی شدن با فریم ورک‌های Dynamic Proxy برای ساده سازی فرآیندهای برنامه نویسی جنبه‌گرا یا AOP، دارا است. در ادامه قصد داریم با نحوه استفاده از این فریم ورک IoC بیشتر آشنا شویم.


دریافت StructureMap

برای دریافت آن نیاز است دستور پاورشل ذیل را در کنسول نیوگت ویژوال استودیو فراخوانی کنید:
 PM> Install-Package structuremap
البته باید دقت داشت که برای استفاده از StructureMap نیاز است به خواص پروژه مراجعه و سپس حالت Client profile را به Full profile تغییر داد تا برنامه قابل کامپایل باشد (در برنامه‌های دسکتاپ البته)؛ از این جهت که StructureMap ارجاعی را به اسمبلی استاندارد System.Web دارد.


آشنایی با ساختار برنامه

ابتدا یک برنامه کنسول را آغاز کرده و سپس یک Class library جدید را به نام Services نیز به آن اضافه کنید. در ادامه کلاس‌ها و اینترفیس‌های زیر را به Class library ایجاد شده، اضافه کنید. سپس از طریق نیوگت به روشی که گفته شد، StructureMap را به پروژه اصلی (ونه پروژه Class library) اضافه نمائید و Target framework آن‌را نیز در حالت Full قرار دهید بجای حالت Client profile.
namespace DI03.Services
{
    public interface IUsersService
    {
        string GetUserEmail(int userId);
    }
}


namespace DI03.Services
{
    public interface IEmailsService
    {
        void SendEmailToUser(int userId, string subject, string body);
    }
}

using System;

namespace DI03.Services
{
    public class UsersService : IUsersService
    {
        public UsersService()
        {
            //هدف صرفا نمایش وهله سازی خودکار این وابستگی است
            Console.WriteLine("UsersService ctor.");
        }

        public string GetUserEmail(int userId)
        {
            //برای مثال دریافت از بانک اطلاعاتی و بازگشت یک نمونه جهت آزمایش برنامه
            return "name@site.com";
        }
    }
}

using System;

namespace DI03.Services
{
    public class EmailsService: IEmailsService
    {
        private readonly IUsersService _usersService;
        public EmailsService(IUsersService usersService)
        {
            Console.WriteLine("EmailsService ctor.");
            _usersService = usersService;
        }

        public void SendEmailToUser(int userId, string subject, string body)
        {
            var email = _usersService.GetUserEmail(userId);
            Console.WriteLine("SendEmailTo({0})", email);
        }
    }
}
در لایه سرویس برنامه، یک سرویس کاربران و یک سرویس ارسال ایمیل تدارک دیده شده‌اند.
سرویس کاربران بر اساس آی دی یک کاربر، برای مثال از بانک اطلاعاتی ایمیل او را بازگشت می‌دهد. سرویس ارسال ایمیل، نیاز به ایمیل کاربری برای ارسال ایمیلی به او دارد. بنابراین وابستگی مورد نیاز خود را از طریق تزریق وابستگی‌ها در سازنده کلاس و وهله سازی شده در خارج از آن (معکوس سازی کنترل)، دریافت می‌کند.
در سازنده‌های هر دو کلاس سرویس نیز از Console.WriteLine استفاده شده‌است تا زمان وهله سازی خودکار آن‌ها را بتوان بهتر مشاهده کرد.
نکته مهمی که در اینجا وجود دارد، بی‌خبری لایه سرویس از وجود IoC Container مورد استفاده است.


استفاده از لایه سرویس و تزریق وابستگی‌ها به کمک  StructureMap

using DI03.Services;
using StructureMap;

namespace DI03
{
    class Program
    {
        static void Main(string[] args)
        {
            // تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود
            ObjectFactory.Initialize(x =>
            {
                x.For<IEmailsService>().Use<EmailsService>();
                x.For<IUsersService>().Use<UsersService>();
            });

            //نمونه‌ای از نحوه استفاده از تزریق وابستگی‌های خودکار
            var emailsService = ObjectFactory.GetInstance<IEmailsService>();
            emailsService.SendEmailToUser(userId: 1, subject: "Test", body: "Hello!");
        }
    }
}
کدهای برنامه را به نحو فوق تغییر دهید. در ابتدا نحوه سیم کشی‌های آغازین برنامه را مشاهده می‌کنید. برای مثال کدهای ObjectFactory.Initialize باید در متدهای آغازین یک پروژه قرار گیرند و تنها یکبار هم نیاز است فراخوانی شوند.
به این ترتیب IoC Container ما زمانیکه قرار است object graph مربوط به IEmailsService درخواستی را تشکیل دهد، خواهد دانست ابتدا به سازنده‌ی کلاس EmailsService می‌رسد. در اینجا برای وهله سازی این کلاس به صورت خودکار، باید وابستگی‌های آن‌را نیز وهله سازی کند. بنابراین بر اساس تنظیمات آغازین برنامه می‌داند که باید از کلاس UsersService برای تزریق خودکار وابستگی‌ها در سازنده کلاس ارسال ایمیل استفاده نماید.
در این حالت اگر برنامه را اجرا کنیم، به خروجی زیر خواهیم رسید:
UsersService ctor.
EmailsService ctor.
SendEmailTo(name@site.com)
بنابراین در اینجا با مفهوم Object graph نیز آشنا شدیم. فقط کافی است وابستگی‌ها را در سازنده‌های کلاس‌ها تعریف کرده و سیم کشی‌های آغازین صحیحی را نیز در ابتدای برنامه معرفی نمائیم. کار وهله سازی چندین سطح با تمام وابستگی‌های متناظر با آن‌ها در اینجا به صورت خودکار انجام خواهد شد و نهایتا یک شیء قابل استفاده بازگشت داده می‌شود.
ابتدایی‌ترین مزیت استفاده از تزریق وابستگی‌ها امکان تعویض آن‌ها است؛ خصوصا در حین Unit testing. اگر کلاسی برای مثال قرار است با شبکه کار کند، می‌توان پیاده سازی آن‌را با یک نمونه اصطلاحا Fake جایگزین کرد و در این نمونه تنها نتیجه‌ی کار را بازگشت داد. کلاس‌های لایه سرویس ما تنها با اینترفیس‌ها کار می‌کنند. این تنظیمات قابل تغییر اولیه IoC container مورد استفاده هستند که مشخص می‌کنند چه کلاس‌هایی باید در سازنده‌های کلاس‌ها تزریق شوند.


تعیین طول عمر اشیاء در StructureMap

برای اینکه بتوان طول عمر اشیاء را بهتر توضیح داد، کلاس سرویس کاربران را به نحو زیر تغییر دهید:
using System;

namespace DI03.Services
{
    public class UsersService : IUsersService
    {
        private int _i;
        public UsersService()
        {
            //هدف صرفا نمایش وهله سازی خودکار این وابستگی است
            Console.WriteLine("UsersService ctor.");
        }

        public string GetUserEmail(int userId)
        {
            _i++;
            Console.WriteLine("i:{0}", _i);
            //برای مثال دریافت از بانک اطلاعاتی و بازگشت یک نمونه جهت آزمایش برنامه
            return "name@site.com";
        }
    }
}
به عبارتی می‌خواهیم بدانیم این کلاس چه زمانی وهله سازی مجدد می‌شود. آیا در حالت فراخوانی ذیل،
 //نمونه‌ای از نحوه استفاده از تزریق وابستگی‌های خودکار
var emailsService1 = ObjectFactory.GetInstance<IEmailsService>();
emailsService1.SendEmailToUser(userId: 1, subject: "Test1", body: "Hello!");

var emailsService2 = ObjectFactory.GetInstance<IEmailsService>();
emailsService2.SendEmailToUser(userId: 1, subject: "Test2", body: "Hello!");
ما شاهد چاپ عدد 2 خواهیم بود یا عدد یک:
 UsersService ctor.
EmailsService ctor.
i:1
SendEmailTo(name@site.com)
UsersService ctor.
EmailsService ctor.
i:1
SendEmailTo(name@site.com)
همانطور که ملاحظه می‌کنید، به ازای هربار فراخوانی ObjectFactory.GetInstance، یک وهله جدید ایجاد شده است. بنابراین مقدار i در هر دو بار مساوی عدد یک است.
اگر به هر دلیلی نیاز بود تا این رویه تغییر کند، می‌توان بر روی طول عمر اشیاء تشکیل شده نیز تاثیر گذار بود. برای مثال تنظیمات آغازین برنامه را به نحو ذیل تغییر دهید:
// تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود
ObjectFactory.Initialize(x =>
{
   x.For<IEmailsService>().Use<EmailsService>();
   x.For<IUsersService>().Singleton().Use<UsersService>();
});
اینبار اگر برنامه را اجرا کنیم، به خروجی ذیل خواهیم رسید:
 UsersService ctor.
EmailsService ctor.
i:1
SendEmailTo(name@site.com)
EmailsService ctor.
i:2
SendEmailTo(name@site.com)
بله. با Singleton معرفی کردن تنظیمات UsersService، تنها یک وهله از این کلاس ایجاد خواهد شد و نهایتا در فراخوانی دوم ObjectFactory.GetInstance، شاهد عدد i مساوی 2 خواهیم بود (چون از یک وهله استفاده شده است).

حالت‌های دیگر تعیین طول عمر مطابق متدهای زیر هستند:
 Singleton()
HttpContextScoped()
HybridHttpOrThreadLocalScoped()
با انتخاب حالت HttpContext، به ازای هر HttpContext ایجاد شده، کلاس معرفی شده یکبار وهله سازی می‌گردد.
در حالت ThreadLocal، به ازای هر Thread، وهله‌ای متفاوت در اختیار مصرف کننده قرار می‌گیرد.
حالت Hybrid ترکیبی است از حالت‌های HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد کرد در غیراینصورت به ThreadLocal سوئیچ می‌کند.

شاید بپرسید که کاربرد مثلا HttpContextScoped در کجا است؟
در یک برنامه وب نیاز است تا یک وهله از DbContext (مثلا Entity framework) را در اختیار کلاس‌های مختلف لایه سرویس قرار داد. به این ترتیب چون هربار new Context صورت نمی‌گیرد، هربار هم اتصال جداگانه‌ای به بانک اطلاعاتی باز نخواهد شد. نتیجه آن رسیدن به یک برنامه سریع، با سربار کم و همچنین کار کردن در یک تراکنش واحد است. چون هربار فراخوانی new Context به معنای ایجاد یک تراکنش جدید است.
همچنین در این برنامه وب قصد نداریم از حالت طول عمر Singleton استفاده کنیم، چون در این حالت یک وهله از Context در اختیار تمام کاربران سایت قرار خواهد گرفت (و DbContext به صورت Thread safe طراحی نشده است). نیاز است به ازای هر کاربر و به ازای طول عمر هر درخواست، تنها یکبار این وهله سازی صورت گیرد. بنابراین در این حالت استفاده از HttpContextScoped توصیه می‌شود. به این ترتیب در طول عمر کوتاه Object graph‌های تشکیل شده، فقط یک وهله از DbContext ایجاد و استفاده خواهد شد که بسیار مقرون به صرفه است.
مزیت دیگر مشخص سازی طول عمر به نحو HttpContextScoped، امکان Dispose خودکار آن به صورت زیر است:
protected void Application_EndRequest(object sender, EventArgs e)  
{  
  ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();  
}

تنظیمات خودکار اولیه در StructureMap

اگر نام اینترفیس‌های شما فقط یک I در ابتدا بیشتر از نام کلاس‌های متناظر با آن‌ها دارد، مثلا مانند ITest و کلاس Test هستند؛ فقط کافی است از قراردادهای پیش فرض StructureMap برای اسکن یک یا چند اسمبلی استفاده کنیم:
 // تنظیمات اولیه برنامه که فقط یکبار باید در طول عمر برنامه انجام شود
ObjectFactory.Initialize(x =>
{
   //x.For<IEmailsService>().Use<EmailsService>();
   //x.For<IUsersService>().Singleton().Use<UsersService>();  
   x.Scan(scan =>
   {
       scan.AssemblyContainingType<IEmailsService>();
       scan.WithDefaultConventions();
   });  
});
در این حالت دیگر نیازی نیست به ازای اینترفیس‌های مختلف و کلاس‌های مرتبط با آن‌ها، تنظیمات اضافه‌تری را تدارک دید. کار یافتن و برقراری اتصالات لازم در اینجا خودکار خواهد بود.


دریافت مثال قسمت جاری
DI03.zip

به روز شده‌ی این مثال‌ها را بر اساس آخرین تغییرات وابستگی‌های آن‌ها از مخزن کد ذیل می‌توانید دریافت کنید:
Dependency-Injection-Samples
 
مطالب
توضیح مثالی از SIMD برای نشان دادن عملکرد آن - SIMD Performance
پیشنیازها

SIMD یا ترجمه آن به فارسی به معنی «تک دستورالعمل و چند داده»، قابلیت آن‌را دارد تا بر روی مقادیر عددی به صورت موازی و با استفاده از پردازنده کار کند. اگر بتوانیم ساختار پروژه‌های خود را به طوری ایجاد کنیم تا بتوانیم از SIMD در پردازش‌های خود استفاده کنیم، سرعت انجام فعالیت‌ها، بسیار زیاد افزایش پیدا خواهند کرد؛ به خصوص این امر در حجم‌های پردازشی زیاد محسوس خواهد بود. البته مدیریت استفاده از منابع و پردازنده نباید فراموش شوند.
اطلاعات لازم از SIMD و نحوه عملکرد آن را می‌توانید در مقاله پیشنیاز بیابید. در این مقاله قصد داریم تا یک مثال ساده از کارآیی SIMD را مطرح کنیم. مثال زیر از مثال SimdSpike الگو برداری شده است و تغییراتی نیز جهت تکمیل شدن آن انجام شده است.
در این مثال می‌خواهیم نمونه کدهایی را با روش‌های معمول اجرا کنیم و زمان اجرای آن را با زمان اجرای همان مثال‌ها با روش SIMD، مقایسه کنیم. 
با استفاده از ویژوال استودیو 2015 آپدیت 3 یک پروژه کنسول با چارچوب دات نت 4.6.1 ایجاد کرده‌ایم. البته می‌توانید ازدیگر نسخه‌ها هم استفاده کنید به شرط آنکه دات نت 4.6x را نصب کرده باشید.

در صورتی که ویژوال استودیوی شما دارای این ورژن و آپدیت نبود، می‌توانید چارچوب دات نت 4.6.1 را جداگانه در سیستم خود نصب نمایید. توجه داشته باشید که برای استفاده از چارچوب دات نت در ویژوال استودیو باید نسخه‌های DevPack یا DeveloperPack را نصب نمایید (دریافت  دات نت 4.6.1 نسخه مخصوص استفاده در ویژال استودیو). 

در پروژه ایجاد شده فایلی به نام Program.cs و در آن کلاس Program وجود دارد. در این کلاس تابع شروع کننده برنامه یعنی Main وجود دارد و برنامه از این تابع شروع خواهد شد.

نمایی از فایل‌های پروژه

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

پس از بررسی وضعیت پشتیبانی از SIMD ، تابعی را که در فایل Utilities.cs نوشته شده است، فراخوانی می‌کنیم. این تابع به بررسی وضعیت تعداد رجیسترهای SIMD و وضعیت انواع نوع‌های داده‌ای در SIMD می‌پردازد. اگر هر نوع داده‌ای از SIMD پشتیبانی کند (که بستگی به نوع پردازنده شما دارد) اندازه هر نوع داده‌ای را در SIMD چاپ می‌کند و در صورت عدم پشتیبانی هر نوع داده‌ای از SIMD مقدار «عدم پشتیبانی SIMD از آن نوع داده‌ای» چاپ خواهد شد. 

  تا به اینجای برنامه کد‌های تابع شروع کننده به صورت زیر خواهد بود. 
using System.Numerics;
using static System.Console;

namespace TestSIMD
{
    class Program
    {
        private const int ArraySize = 7680 * 4320;
        static void Main(string[] args)
        {
            // بررسی وضعیت پشتیبانی از SIMD
            if (!Vector.IsHardwareAccelerated)
            {
                WriteLine("Hardware acceleration not supported.");
                WriteLine();
                return; // عدم پشتیبانی و خاتمه برنامه
            }
            WriteLine("Hardware acceleration is supported"); // اعلام پشتیبانی از SIMD
            WriteLine();

            // بررسی وضعیت نوع‌های داده ای در مشخصات سخت افزاری SIMD
            Utilities.PrintHardwareSpecificSimdEffectiveness();

            //به منظور عدم خروج از برنامه و دیدن نتایج آزمایش
            WriteLine("Press any key to exit");
            ReadKey();
        }
    }
}
اجرای برنامه هم به صورت زیر به نمایش در خواهد آمد. 

در فایل Utilities.cs، توابع دیگری هم وجود دارند که کارآیی هر یک به صورت توضیح در بالای هر تابع نوشته شده است. این توابع برای تولید یک نوع داده‌ای تصادفی و ایجاد آرایه‌ای از نوع داده‌ای به صورت تصادفی به کار برده می‌شوند. می توانید در سورس برنامه این توضیحات را مشاهده کنید.
تا به اینجا تنها به بررسی پشتیبانی سخت افزاری از SIMD پرداختیم و همچنین توانستیم نوع‌های داده‌ای را که SIMD در سخت افزار ما پشتیبانی می‌کند، شناسایی کنیم و اندازه رجیستر‌های آنها را بیابیم.
حال به بررسی عملکرد توابع SIMD می‌پردازیم و با نوشتن چند تابع، زمان اجرای محاسباتی آنها را با نوشتن همان توابع در حالت معمولی و ساده مقایسه می‌کنیم.
 برای انجام مقایسه، زمان اجرای یک عملیات را در حالت معمول، با زمان اجرای همان عملیات در حالت SIMD بررسی می‌کنیم. هر عملیات را 3 مرتبه پشت سر هم اجرا می‌کنیم و زمان آنها را ثبت می‌کنیم تا تفاوت زمان اجرا را با تکرار عملیات نیز مشاهده کنیم. توابعی که آزمایشات را انجام می‌دهند و زمان اجرا را ثبت و نمایش می‌دهند، در فایل PerformanceTests.cs و در کلاس PerformanceTests قرار دارند و از توابع سه کلاس دیگر که عملیات در آن نوشته شده‌اند، استفاده می‌کنند.
  • فایل IntSimdProcessor.cs
    • در این فایل کلاسی به نام IntSimdProcessor قرار دارد که شامل 6 تابع می‌باشد و این تابع‌ها با نوع داده‌ای صحیح یا همان Integer کار می‌کنند. نام کلاس هم به همین خاطر نام گذاری شده است. 
    • این 6 تابع در کل 3 عملیات را شامل عملیات‌های زیر انجام می‌دهند. یکبار در حالت معمولی و یکبار با استفاده از توابع SIMD این کار را انجام می‌دهند:
      • پیدا کردن بزرگترین و کوچکترین عدد در آرایه
      • جمع عناصر دو آرایه با هم با استفاده از یک آرایه کمکی که نتیجه در آرایه کمکی ریخته می‌شود
      • جمع عناصر دو آرایه بدون استفاده از آرایه کمکی که مجموع در آرایه اول ریخته می‌شود
    • در بالای هر تابع در این فایل توضیحات لازم درباره‌ی فعالیت آن تابع ذکر شده است.
 
  • فایل FloatSimdProcessor.cs
    • در این فایل کلاسی با نام FloatSimdProcessor قرار دارد که همانطور که از نام کلاس پیداست، توابعی برای کار بر روی اعداد از نوع داده‌ای float در آن نوشته شده‌اند.
    • در این کلاس هم 6 تابع برای انجام 3 عملیات زیر نوشته شده است که به ازای هر عملیات دو تابع یکی در حالت معمولی و یکی در حالت SIMD نوشته شده است.
      • جمع دو آرایه با استفاده از یک آرایه کمکی - مجموع در آرایه کمکی ریخته می‌شود
      • جمع دو آرایه اول ورودی - مجموع در آرایه سوم ریخته می‌شود
      • جمع دو آرایه بدون استفاده از آرایه کمکی - مجموع در آرایه اول ریخته می‌شود
    • در آزمایشات نوشته شده در کلاس PerformanceTests  تنها از عملیات آخری استفاده شده است و از دو عملیات اول استفاده نشده است که در صورت تمایل می‌توانید از دیگر عملیات‌ها نیز استفاده کنید.
    • در بالای هر تابع در این فایل توضیحات لازم درباره‌ی فعالیت آن تابع نیز ذکر شده است.
 
  • فایل UShortSimdProcessor.cs
    • در این فایل کلاسی با نام UShortSimdProcessor قرار دارد و همانطور که از نام کلاس پیداست، توابعی برای کار بر روی اعداد از نوع داده‌ای ushort یا همان اعداد صحیح کوچک بدون علامت نوشته شده‌اند.
    • در این کلاس 12 تابع برای انجام 6 عملیات زیر نوشته شده‌است که به ازای هر عملیات، دو تابع یکی در حالت معمولی و یکی در حالت SIMD نوشته شده است.
      • جمع دو آرایه اول ورودی که مجموع در آرایه سوم ریخته می‌شود
      • جمع دو آرایه بدون استفاده از آرایه کمکی که مجموع در آرایه اول ریخته می‌شود
      • بدست آوردن کمترین و بیشترین مقدار در یک آرایه اعداد صحیح کوچک بدون علامت
      • جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی
      • جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی بدون بررسی سرریز (Overflow)
      • محاسبه میانگین و بدست آوردن کمترین و بیشترین مقدار در یک آرایه اعداد صحیح کوچک بدون علامت
    • در بالای هر تابع در این فایل توضیحات لازم درباره‌ی فعالیت آن تابع ذکر شده است.
 
حال در کلاس PerformanceTests برای انجام آزمایشات و مقایسه زمان اجرا، 10 تابع وجود دارند که 10 عملیات مختلف را بر روی 3 نوع داده‌ای، اجرا می‌کنند. 3 عملیات از کلاس IntSimdProcessor و یک عملیات از کلاس FloatSimdProcessor و 6 عملیات از کلاس UShortSimdProcessor را مورد آزمایش قرار داده‌ایم که در مجموع شامل 10 آزمایش در 10 تابع مختلف شده است.
public static void TestIntArrayAdditionFunctions(int testSetSize) {
    WriteLine();
    Write("Testing int array addition, generating test data...");
    var intsOne = GetRandomIntArray(testSetSize); //تولید آرایه عددی به صورت تصادفی
    var intsTwo = GetRandomIntArray(testSetSize);
    WriteLine($" done, testing...");// پایان تولید آرایه‌ها و شروع پردازش
    var naiveTimesMs = new List<long>(); // تعریف لیستی برای ریختن زمان پاسخ دهی در حالت ساده و معمولی
    var hwTimesMs = new List<long>(); // تعریف لیستی برای ریختن زمان پاسخ دهی در حالت SIMD و سخت افزاری 
    for (var i = 0; i < 3; i++) { // ایجاد حلقه برای تکرار محاسبات برای اندازه گیری زمان در حالت تکراری
        stopwatch.Restart();//شروع ثبت زمان
        var result = IntSimdProcessor.NaiveSumFunc(intsOne, intsTwo);//اجرای تابع جمع دو آرایه
        var naiveTimeMs = stopwatch.ElapsedMilliseconds;//ثبت زمان
        naiveTimesMs.Add(naiveTimeMs);//افزودن زمان ثبت شده به لیست زمان‌های ساده و معمول
        WriteLine($"Naive analysis took:                {naiveTimeMs}ms (last value = {result.Last()}).");

        stopwatch.Restart();//شروع ثبت زمان
        result = IntSimdProcessor.HWAcceleratedSumFunc(intsOne, intsTwo);//اجرای تابع جمع دو آرایه در حالت سخت افزاری
        var hwTimeMs = stopwatch.ElapsedMilliseconds;//ثبت زمان
        hwTimesMs.Add(hwTimeMs);//افزودن زمان به لیست زمان‌های سخت افزاری
        WriteLine($"Hareware accelerated analysis took: {hwTimeMs}ms (last value = {result.Last()}).");
    }//پایان حلقه و چاپ نتایج
    WriteLine("Int array addition:");
    WriteLine($"Naive method average time:          {naiveTimesMs.Average():.##}");
    WriteLine($"HW accelerated method average time: {hwTimesMs.Average():.##}");
    WriteLine($"Hardware speedup:                   {naiveTimesMs.Average() / hwTimesMs.Average():P}%");
}
در بالا تکه کدی مربوط به تابع آزمایش اول از کلاس PerformanceTests قرار دارد و وظیفه دارد عملیات جمع دو آرایه را با استفاده از یک آرایه کمکی اعداد صحیح، هم در حالت معمولی و هم در حالت SIMD انجام دهد و زمان اجرای آنها را ثبت و نمایش دهد تا بتوانیم این زمان اجرا‌ها را با هم مقایسه کنیم.
ساختار و روند اجرای کلیه آزمایش‌ها و توابع در کلاس PerformanceTests با یکدیگر یکسان است و از یک stopwatch یا همان کرنومتر برای محاسبه زمان اجرا استفاده شده است.
هر کدام از این توابع یک عملیات را مورد بررسی قرار می‌دهند و هر عملیات را 3 مرتبه اجرا می‌کنند تا زمان تکرار اجرا نیز مورد مقایسه قرار گیرد.

نام تابع ذکر شده نشان دهنده آزمایش بر روی آرایه اعداد صحیح یا همان Integer می‌باشد که شامل یک پارامتر ورودی از نوع عدد صحیح می‌باشد. این پارامتر ورودی نشان دهنده اندازه هر آرایه‌ای می‌باشد که قرار است تولید شود.  

TestIntArrayAdditionFunctions(int testSetSize)

در قدم اول این تابع، باید آرایه‌ها را تولید کنیم که کد آن به صورت زیر است.

Write("Testing int array addition, generating test data...");
var intsOne = GetRandomIntArray(testSetSize);
var intsTwo = GetRandomIntArray(testSetSize);
WriteLine($" done, testing...");

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

در پایان تولید آرایه‌ها، اتمام تولید و ایجاد آرایه‌ها را با چاپ در خروجی اعلام میکنیم.

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

var naiveTimesMs = new List<long>();
var hwTimesMs = new List<long>();

سپس با ایجاد حلقه ای از 0 تا 3 که در کل 3 مرتبه اجرا می‌شود عملیات را تکرار و زمان آن را ثبت می‌کنیم. 

for (var i = 0; i < 3; i++)

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

stopwatch.Restart();
var result = IntSimdProcessor.NaiveSumFunc(intsOne, intsTwo);
var naiveTimeMs = stopwatch.ElapsedMilliseconds;
naiveTimesMs.Add(naiveTimeMs);
WriteLine($"Naive analysis took:                {naiveTimeMs}ms (last value = {result.Last()}).");

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

stopwatch.Restart();
result = IntSimdProcessor.HWAcceleratedSumFunc(intsOne, intsTwo);
var hwTimeMs = stopwatch.ElapsedMilliseconds;
hwTimesMs.Add(hwTimeMs);
WriteLine($"Hareware accelerated analysis took: {hwTimeMs}ms (last value = {result.Last()}).");

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

WriteLine($"Naive method average time:          {naiveTimesMs.Average():.##}");
WriteLine($"HW accelerated method average time: {hwTimesMs.Average():.##}");
WriteLine($"Hardware speedup:                   {naiveTimesMs.Average() / hwTimesMs.Average():P}%");

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

برای اجرای تمامی آزمایش‌ها، کلیه توابع نوشته شده در کلاس PerformanceTests را در کلاس Program و در تابع Main که تابع شروع کننده برنامه می‌باشد، پس از بررسی وضعیت نوع‌های داده‌ای قرار می‌دهیم.

تصویر مربوط به اجرای کامل برنامه را می‌توانید مشاهده می‌کنید. 

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

زمان‌ها در جدول به میلی ثانیه می‌باشد.

ردیف

عملیات

دور اول

دور دوم

دور سوم

میانگین حالت ساده

میانگین حالت SIMD

درحالت ساده

درحالت SIMD

درحالت ساده

درحالت SIMD

درحالت ساده

درحالت SIMD

1

جمع دو آرایه با استفاده از یک آرایه کمکی در اعداد صحیح

157

131

128

131

128

138

137.67

133.33

2

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

122

133

99

99

99

93

106.67

108.33

3

جمع دو آرایه بدون استفاده از آرایه کمکی در اعداد صحیح

83

73

86

88

78

81

82.33

80.67

4

جمع دو آرایه اول ورودی - مجموع در آرایه سوم ریخته می‌شود - در اعداد صحیح کوچک بدون علامت

58

63

50

48

58

46

55.33

52.33

5

جمع دو آرایه بدون استفاده از آرایه کمکی در اعداد صحیح کوچک بدون علامت

55

40

53

36

53

46

53.67

40.67

6

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

91

36

91

39

90.67

38

90.66

38

7

بدست آوردن کمترین و بیشترین مقدار در یک آرایه اعداد صحیح کوچک بدون علامت

90

20

89

19

88

18

89

19

8

جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی

33

309

32

263

31

291

32

287.67

9

جمع عناصر آرایه ورودی و ذخیره مجموع آنها در یک متغیر کمکی بدون بررسی سرریز

30

13

29

13

30

12

29.67

12.67

10

محاسبه میانگین و بدست آوردن کمترین و بیشترین مقدار در آرایه اعداد صحیح کوچک بدون علامت

89

50

90

51

90

49

89.57

50



سورس کامل برنامه را که شامل تغییراتی در توابع برای بهبود و اضافه شدن کامنت برای فهم بیشتر کدها می‌باشد، در زیر می‌توانید دریافت کنید: 
   TestSIMD.zip  

نظرات مطالب
خواندنی‌های 25 اردیبهشت
1 - خیر. jQuery یک کتابخانه اسکریپتی است و کاری با محتوای سایت شما ندارد مگر اینکه خودتان بر اساس آن تغییری را اعمال کنید.
2 - بله. همین برچسب‌هایی را که ملاحظه می‌کنید، کلمات کلیدی مورد نظر شما هستند.
3- یک ویجت از نوع فید اضافه کنید. سپس آدرس فید کامنت‌های سایت را به آن بدهید (هر سایت حداقل دو فید دارد، فید مطالب و فید نظرات). اکنون لیست آخرین 5 نظر ارائه شده را خواهید داشت.
4 و 5 - اگر با این دو مورد مشکل دارید توصیه می‌کنم از windows live writer استفاده کنید که با بلاگر هم سازگار است:
http://baghbanbashi.blogspot.com/2009/01/live-writer-2009-features.html
http://baghbanbashi.blogspot.com/2006/12/windows-live-writer.html
نظرات اشتراک‌ها
واژه‌نامه Agile
سلام،
سال نو مبارک.
از لطف شما سپاسگزارم.
با بهترین آرزوها

نظرات مطالب
ASP.NET MVC #8
میشه لطف کنید بگید اگر helper  رو جایی دیگه غیر از App_code  داشته باشیم، مثلا توی یک فولدر، چه جوری باید ازش استفاده کرد؟