مطالب
ایجاد بک آپ برای دیتابیس با استفاده از CMD
در پست قبلی مهندس نصیری با استفاده کتابخانه ExpressMaint و Schedule  ویندوز یه سیستم بک آپ گیری را پیاده کرده اند، در این بین با توجه به وجود SMO میتوان از طریق خط فرمان ویندوز و کمک گیری از کتابخانه 7ZIP و نوشتن فرامین مربوط و ایجاد بچ فایل و افزودن به Schedule  ویندوز، نیز میتوان بصورت زیر نیز این پیاده سازی را انجام داد، که بنا بر نیاز اسکریپت بک آپگیری را نوشت:
echo off
cls
rem --------------- Variables --------------- 
set packName=DbName
set connection=-S Server -U User -P Password
set path=PathForSaveBackup
rem --------------- Variables --------------- 

echo create backup
sqlcmd %connection% -Q "BACKUP DATABASE %packName% TO DISK = '%path%\%packName%.bak'"

echo compress backup file
..\tools\7za.exe a %path%\%packName%.7z %path%\%packName%.bak

echo delete backup file
del %path%\%packName%.bak

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


۱. تنظیمات GUI Theme :
از منوی File بر روی Settings کلیک می‌کنیم و بر روی Plugin کلیک کرده در قسمت جستجو عبارت مقابل ( Material Theme UI ) را  جستجو کرده و بر روی Install jetBrains Plugin کلیک می‌کنیم که Plugin مورد نظر ما نصب خواهد شد که بعد نصب اندروید استودیو را Restart می‌کنیم . (تنظیمات طبق عکس زیر انجام گردد) 
دسترسی به مخزن گیت هاب ‍   Material Theme UI for jetBrains  
⌘⇧a → "Plugins" → ↩ → ⌃⌥b → <search> → "Material Theme UI" → [Install plugin] → ⌃⌥c → ⎋ → <restart>

۲. تنظیمات Editor Scheme :
از منوی File بر روی Settings کلیک کرده و از قسمت Editor بر روی Color Scheme کلیک کرده و از قسمت Scheme که chroMATERIAL را انتخاب کرده و بر روی Apply/OK  کلیک می‌کنیم.
⌘⇧a → "Color Scheme" → [3. ChroMATERIAL]
و از قسمت Color Scheme Font که Use color scheme font instead را انتخاب و فونت را Lucida Snas Typewriter انتخاب و Apply/ok می‌کنیم.

۳. تنظیمات HOLO Logcat :
به مسیر زیر مراجعه کرده Preferences → Editor → Colors & Fonts → Android Logcat و تنظیمات Logcat را طبق سلیقه خود مشخص می‌کنیم و در نهایت Apply/ok می‌کنیم .
⌘⇧a → "Android Logcat" → [Save as...] → "ChroMATERIAL + HOLO"¹ → ↩ → <set foreground colors as in the table ↑> → ⎋
Type   : Color
verbose: #BBB debug: #33B5E5 info: #9C0 assert: #A6C error: #F44 warning: #FB3 

ساخت یک محیط توسعه زیبا در اندروید استودیو
مطالب
استفاده از افزونه‌ی 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
مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت هفتم
در قسمت ششم، یک صفحه ساده برای لاگین نوشتیم که عملا کار خاصی نمی‌کرد. حال می‌خواهیم در این قسمت روی UI آن کمی کار کنیم. دقت کنید که هدف این قسمت، آموزش زیبا سازی صفحات نیست؛ بلکه هدف، آشنایی شما با تنظیمات مهم UI است.
صفحه Login ای که در قسمت قبل نوشته شد، خود یک Content Page است و دارای یک Stack Layout با چینش عمودی است. داخل آن دو Entry برای گرفتن نام و نام کاربری وجود دارد و یک Button.
مشکلی که صفحه لاگین دارد این است که اگر در یک تبلت 10 اینچی هم باز شود، عرض Entry‌ها و Button هم 10 اینچ می‌شوند که ما قطعا این را نمی‌خواهیم. اینکه چه می‌خواهیم خود یک سوال است! ولی یکی از راه‌های متداول و آسان که برای صفحه لاگین هم قابلیت استفاده دارد این است که از المان‌های صفحه درخواست می‌شود مثلا 5 سانتی متر باشند. حال اگر صفحه نمایش بیشتر از 5 سانتی متر فضا داشت، دلیلی و ارزشی برای بیشتر بزرگ شدن المان‌های روی صفحه نیست. البته اگر صفحه نمایش کوچک‌تر از 5 سانتی متر هم باشد، اینکه المان‌های روی صفحه اصرار به 5 سانتی متر ماندن کنند، نتیجه جالبی نخواهد داشت! برای دستیابی به بهترین نتیجه، شما می‌توانید از WidthRequest و HeightRequest استفاده کنید. وقتی WidthRequest را برابر با 5 سانتی متر قرار دهید، در واقع دارید درخواست می‌کنید که این مقدار شود. اگر فضا بیشتر شود، المان مربوطه بزرگ‌تر از 5 سانتی متر نخواهد شد. اگر فضا کل فضایی که دارد، کمتر از 5 سانتی متر باشد و مثلا 4 سانتی متر باشد، آن هم 4 سانتی متر خواهد شد.
برای شروع سؤال پیش می‌آید که این WidthRequest را به کدامیک از Tag‌ها بدهیم؟ Content Page یا Stack Layout یا هر کدام از Entry‌ها و Button لاگین؟ اساسا وظیفه Layout به عهده Page نیست. در اکثر مواقع، اینکار با خود Stack Layout انجام می‌شود ( یا در سایر مثال‌ها با Grid - Flex Layout و ... ) تنظیم WidthRequest برابر با 5 سانتی متر، آن هم برای تک تک Entry‌ها و Button مربوطه هم اساسا ایده جالبی نیست.
برای این که Width Request برابر با 5 سانتی متر شود، باید به آن مقدار 320 داده شود. هر 64 تا می‌شود یک سانتی متر و 320 = 64 * 5
اگر Width Request را در مثال Login برابر با 320 قرار دهید، می‌بینید که کوچک نمی‌شود و کماکان کل فضای صفحه را می‌گیرد! علت این است که هر المان روی صفحه، از جمله Stack Layout، علاوه بر WidthRequest و HeightRequest که عرض و طول Child را نسبت به Parent مشخص می‌کند، دارای VerticalOptions و HorizontalOptions نیز هست. VerticalOptions و HorizontalOptions در کنار WidthRequest و HeightRequest، رابطه بین یک Parent و Child را مشخص می‌کنند (در این مثال رابطه بین Content Page و Stack Layout به عنوان Parent و Child). به صورت پیش فرض هر Child ( در اینجا Stack Layout ) به صورت عمودی و افقی Parent اش ( در اینجا Content Page ) را Fill می‌کند. علت پیش فرض بودن این رفتار این است که کمترین سربار ممکن را از لحاظ Performance دارد و خیلی از جاها هم مفید است. در اینجا چون Stack Layout قصد دارد Parent خود را Fill کند، دیگر WidthRequest برایش معنی ندارد. در ادامه من VerticalOptions را برابر با Start و HorizontalOptions را برابر با Center می کنم. این باعث می‌شود که Stack Layout ما از لحاظ عمودی در بالا و از لحاظ افقی در وسط Content Page قرار بگیرد و بیش از پنج سانتی متر بزرگ نشود و در صورتیکه Content Page خود فضای کمی داشته باشد ( مثلا در یک گوشی موبایل با صفحه نمایش خیلی کوچک باشد )، Stack Layout حتی از پنج سانتی متر کوچک‌تر هم می‌شود.
دقت کنید که دو Entry و Button داخل Stack Layout نیز Fill هستند، پس داخل Parent خود یعنی Stack Layout را پر می‌کنند. اگر به Stack Layout یک Background دهید، درک بهتری از آن چه که دارد اتفاق می‌افتد، خواهید داشت.
<StackLayout
      BackgroundColor="LightYellow"
      HorizontalOptions="Center"
      Orientation="Vertical"
      VerticalOptions="Start"
      WidthRequest="320">

مشکل دیگری که وجود دارد این است که اگر بابت کمبود فضا، Stack Layout کوچکتر از 5 سانتی متر شود، از بغل، کامل، به کناره‌های Parent خود می‌چسبد که اصلا جالب نیست. برای رفع این مشکل می‌توانیم از Padding (حاشیه داخلی) و Margin (حاشیه خارجی) استفاده کنیم. در عکس زیر، Background مربوط به Content Page را قرمز و Stack Layout را آبی کرده‌ایم. با اختصاص دادن یک سانتی متر (64) به Padding و نیم سانت به Margin، می‌توانید در عکس زیر تاثیر آن را ببینید:

در سمت راست، بالا و چپ Stack Layout، به اندازه نیم سانت حاشیه قرمز می‌بینید که تاثیر Margin است. همچنین المان‌های داخل Stack Layout نیز هر کدام یک سانت از اطراف فاصله دارند که تاثیر Padding است. در پایین Stack Layout، تا جایی که Content Page بزرگ شود، فضای قرمز می‌بینید، نیم سانت از این فضای قرمز به خاطر Margin بوده، ولی باقی مربوط به این است که VerticalOptions را برای Stack Layout، بر روی Start تنظیم کرده‌ایم که باعث می‌شود Stack Layout بالای Content Page قرار بگیرد و زیر آن تماما قرمز شود. حتی اگر "عرض" صفحه نمایش بزرگتر از عکس قبلی می‌بود، فضای قرمز از راست و چپ بیشتر می‌شد که نیم سانت آن بابت Margin بوده و بیشتر از آن مربوط به HorizontalOptions است که برای Stack Layout روی Center تنظیم شده.

با توجه به اینکه Page - Layout - Control‌ها در Android-iOS-Windows به معادل‌های Native خود تبدیل می‌شوند و برای مثال ظاهر Button در این سه پلتفرم متفاوت است، ممکن است Padding ای که به یک کنترل داده ایم و در اندروید خوب به نظر می‌رسد، در iOS مقدار دیگری را لازم داشته باشد. مثلا در Android مقدار 2 و در iOS مقدار 3 لازم داشته باشد. یا ممکن است این مقدار به ازای این که در موبایل، تبلت یا دسکتاپ باشیم، لازم باشد که متفاوت باشد. در Xaml ما دو امکان OnPlatform و OnIdiom را داریم که هر Property اعم از Font Size - Padding - Text و ... را می‌توان به ازای هر شرایطی با مقداری متفاوت مقدار دهی نمود. برای مثال داریم:

<StackLayout
        Margin="{OnPlatform Android=3,
                            iOS='0,0,20,0',
                            UWP='2'}"
        Padding="{OnIdiom Default=2,
                          Tablet=3}" ...

البته Property‌های مهم دیگری نیز در UI تاثیر گذار هستند. برای مثال Opacity برای تنظیم شفافیت، FlowDirection برای تنظیم Right to left و Left to right بودن و خیلی Property‌های دیگه که به مرور و با بیشتر کار کردن با Xamarin Forms با آن‌ها آشنا می‌شوید.

در قسمت بعدی به نوشتن منطق می‌پردازیم؛ نحوه نمایش دادن خطا و رفتن از صفحه‌ای به صفحه‌ی دیگر. 

مطالب
Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر
از زمان Blazor 5x، پشتیبانی توکار از آپلود فایل‌ها، به آن اضافه شده‌است و پیش از آن می‌بایستی از کامپوننت‌های ثالث استفاده می‌شد. در این قسمت نحوه‌ی استفاده از کامپوننت آپلود فایل‌های Blazor را بررسی می‌کنیم. همچنین یک نمونه مثال، از فرم‌های master-details را نیز با هم مرور خواهیم کرد.



افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق

در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آن‌را تکمیل کرده‌ایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه می‌کنیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
</div>

@code
{
    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {

    }
}
- ذکر ویژگی multiple در اینجا سبب می‌شود تا بتوان بیش از یک فایل را هربار انتخاب و آپلود کرد.
- در این کامپوننت، رویداد OnChange، پس از تغییر مجموعه‌ی فایل‌های اضافه شده‌ی به آن، فراخوانی می‌شود و آرگومانی از نوع InputFileChangeEventArgs را دریافت می‌کند.


افزودن لیست فایل‌های انتخابی به HotelRoomDTO

تا اینجا اگر به BlazorServer.Models\HotelRoomDTO.cs مراجعه کنیم (کلاسی که مدل UI فرم ثبت اطلاعات اتاق را فراهم می‌کند)، امکان افزودن لیست تصاویر انتخابی به آن وجود ندارد. به همین جهت در این کلاس، تغییر زیر را اعمال می‌کنیم:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class HotelRoomDTO
    {
        // ... 
        public virtual ICollection<HotelRoomImageDTO> HotelRoomImages { get; set; } = new List<HotelRoomImageDTO>();
    }
}
HotelRoomImageDTO را در قسمت قبل اضافه کردیم. متناظر با ICollection فوق، چنین خاصیتی در موجودیت HotelRoom که از نوع <ICollection<HotelRoomImage است نیز تعریف شده‌است تا بتوان به ازای هر اتاق، مشخصات تعدادی تصویر را در بانک اطلاعاتی ذخیره کرد.


تکمیل متد رویدادگردان HandleImageUpload

در ادامه، لیست فایل‌ها‌ی انتخاب شده‌ی توسط کاربر را دریافت کرده و آن‌ها را آپلود می‌کنیم:
@inject IHotelRoomService HotelRoomService
@inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime
@inject IFileUploadService FileUploadService
@inject IWebHostEnvironment WebHostEnvironment

@code
{
    // ...

    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 5);
        if (args.FileCount == 0 || files.Count == 0)
        {
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            await JsRuntime.ToastrError("Please select .jpg/.jpeg/.png files only.");
            return;
        }

        foreach (var file in files)
        {
            var uploadedImageUrl = await FileUploadService.UploadFileAsync(file, WebHostEnvironment.WebRootPath, "Uploads");
            HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
        }
    }
}
- در اینجا نیاز به تزریق چند سرویس جدید هست؛ مانند IFileUploadService که در قسمت قبل تکمیل کردیم و سرویس توکار IWebHostEnvironment. به همین جهت به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضاهای نام متناظر زیر را اضافه می‌کنیم:
@using Microsoft.AspNetCore.Hosting
@using System.Linq
@using System.IO
برای مثال سرویس IWebHostEnvironment که از آن برای دسترسی به WebRootPath یا محل قرارگیری پوشه‌ی wwwroot استفاده می‌کنیم، در فضای نام Microsoft.AspNetCore.Hosting قرار دارد و یا متد Path.GetExtension در فضای نام System.IO و متد الحاقی Contains با دو پارامتر استفاده شده، در فضای نام System.Linq قرار دارند.
- متد ()args.GetMultipleFiles، امکان دسترسی به فایل‌های انتخابی توسط کاربر را میسر می‌کند که خروجی آن از نوع <IReadOnlyList<IBrowserFile است. در قسمت قبل، سرویس آپلود فایل‌هایی را که تکمیل کردیم، امکان آپلود یک IBrowserFile را به سرور میسر می‌کند. اگر متد ()GetMultipleFiles را بدون پارامتری فراخوانی کنیم، حداکثر 10 فایل را قبول می‌کند و اگر تعداد بیشتری انتخاب شده باشد، یک استثناء را صادر خواهد کرد.
- سپس بر اساس پسوند فایل‌های دریافتی، آن‌ها را صرفا به فایل‌های تصویری محدود کرده‌ایم.
- در آخر، لیست فایل‌های دریافتی را یکی یکی به سرور آپلود کرده و Url دسترسی به آن‌ها را به لیست HotelRoomImages اضافه می‌کنیم. فایل‌های آپلود شده در پوشه‌ی BlazorServer.App\wwwroot\Uploads قابل مشاهده هستند.


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


در ادامه می‌خواهیم پس از آپلود فایل‌ها، آن‌ها را در ذیل کامپوننت InputFile نمایش دهیم. برای اینکار در ابتدا به فایل wwwroot\css\site.css مراجعه کرده و شیوه نامه‌ی نمایش تصاویر و عناوین آن‌ها را اضافه می‌کنیم:
.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
سپس بر روی لیست HotelRoomModel.HotelRoomImages که در متد HandleImageUpload آن‌را تکمیل کردیم، حلقه‌ای را ایجاد کرده و تصاویر را بر اساس RoomImageUrl آن‌ها، نمایش می‌دهیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
    <div class="row">
    @if (HotelRoomModel.HotelRoomImages.Count > 0)
    {
        var serial = 1;
        foreach (var roomImage in HotelRoomModel.HotelRoomImages)
        {
            <div class="col-md-2 mt-3">
                <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; ">
                   <span class="room-image-title">@serial</span>
                </div>
                <button type="button" class="btn btn-outline-danger btn-block mt-4">Delete</button>
            </div>
            serial++;
        }
    }
    </div>
</div>

ذخیره سازی اطلاعات تصاویر آپلودی یک اتاق در بانک اطلاعاتی

تا اینجا موفق شدیم تصاویر انتخابی کاربر را آپلود کرده و همچنین لیست آن‌ها را نیز نمایش دهیم. در ادامه نیاز است تا این اطلاعات را در بانک اطلاعاتی ثبت کنیم. به همین جهت ابتدا سرویس IHotelRoomImageService را که در قسمت قبل تکمیل کردیم، به کامپوننت جاری تزریق می‌کنیم و سپس با استفاده از متد CreateHotelRoomImageAsync، رکوردهای تصویر متناظر با اتاق ثبت شده را اضافه می‌کنیم:
// ...
@inject IHotelRoomImageService HotelRoomImageService


@code
{
    // ...

    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages)
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
}
در حین آپلود فایل‌ها، فقط خاصیت RoomImageUrl را مقدار دهی کردیم:
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
در اینجا RoomId هر imageDto را نیز بر اساس Id واقعی اتاق ثبت شده‌ی جاری، تکمیل کرده و سپس آن‌را به CreateHotelRoomImageAsync ارسال می‌کنیم.

محل فراخوانی AddHotelRoomImageAsync فوق، در متد HandleHotelRoomUpsert است که در قسمت‌های قبل تکمیل کردیم. در اینجا پس از ثبت اطلاعات اتاق در بانک اطلاعاتی است که به Id آن دسترسی پیدا می‌کنیم:
private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       var createdRoomDto = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
       await AddHotelRoomImageAsync(createdRoomDto);
       await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully.");

       // ... 
    }
اکنون اگر اطلاعات اتاق جدیدی را تکمیل کرده و تصاویری را نیز به آن انتساب دهیم، با کلیک بر روی دکمه‌ی ثبت، ابتدا اطلاعات این اتاق در بانک اطلاعاتی ثبت شده و Id آن به‌دست می‌آید، سپس رکوردهای تصویر آن جداگانه ذخیره خواهند شد.

یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطه‌ی one-to-many تعریف شده‌ی توسط EF-Core، اگر لیست HotelRoomImages موجودیت اتاق تعریف شده و در حال ثبت نیز مقدار دهی شده باشد، به صورت خودکار جزئی از این رابطه و تنها در یک رفت و برگشت، ثبت می‌شود. یعنی همان متد CreateHotelRoomAsync، قابلیت ثبت خودکار اطلاعات خاصیت HotelRoomImages موجودیت اتاق را نیز دارا است.


نمایش تصاویر یک اتاق، در حالت ویرایش رکورد آن

تا اینجا فقط حالت ثبت یک رکورد جدید را پوشش دادیم. در این حالت اگر به لیست اتاق‌های ثبت شده مراجعه کرده و بر روی دکمه‌ی edit یکی از آن‌ها کلیک کنیم، به صفحه‌ی ویرایش رکورد منتقل خواهیم شد؛ اما این صفحه، فاقد اطلاعات تصاویر منتسب به آن رکورد است.
علت اینجا است که در حین ویرایش اطلاعات، در متد OnInitializedAsync، هرچند اطلاعات یک اتاق را از بانک اطلاعاتی دریافت کرده و آن‌را تبدیل به Dto آن می‌کنیم که سبب نمایش جزئیات هر خاصیت در فیلد متصل به آن در فرم جاری می‌شود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
        }
        // ...
    }
اما چون یک رابطه‌ی one-to-many بین اتاق و تصاویر آن برقرار است، نیاز است این رابطه را از طریق eager-loading و فراخوانی متد Include، واکشی کنیم تا اینبار زمانیکه GetHotelRoomAsync فراخوانی می‌شود، به همراه اطلاعات navigation property لیست تصاویر اتاق (HotelRoomImages) نیز باشد.
بنابراین به فایل BlazorServer\BlazorServer.Services\HotelRoomService.cs مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        // ...
 
        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }
    }
}
در اینجا تنها تغییری که صورت گرفته، استفاده از متد Include(x => x.HotelRoomImages) است؛ تا هنگامیکه اطلاعات یک اتاق را واکشی می‌کنیم، به صورت خودکار اطلاعات تصاویر مرتبط به آن نیز واکشی گردد و سپس توسط AutoMapper، به Dto آن انتساب داده شود (یعنی انتساب HotelRoomImages موجودیت اتاق، به همین خاصیت در DTO آن). این انتساب، سبب به روز رسانی خودکار UI نیز می‌شود. یعنی برای نمایش تصاویر مرتبط با یک اتاق، همان کدهای قبلی که پیشتر داشتیم، هنوز هم کار می‌کنند.


افزودن تصاویر جدید، در حین ویرایش یک رکورد

پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون می‌خواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شده‌ی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد HandleHotelRoomUpsert مراجعه کرده و از متد AddHotelRoomImageAsync در قسمت به روز رسانی آن استفاده می‌کنیم:
private async Task HandleHotelRoomUpsert()
{
   //...

   // Update Mode
   var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);
   await AddHotelRoomImageAsync(updatedRoomDto);
   await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");

   //...
}
مشکل! اگر از این روش استفاده کنیم، هربار به روز رسانی اطلاعات یک جدول، به همراه ثبت رکوردهای تکراری نمایش داده شده‌ی در حالت ویرایش هم خواهند بود. برای مثال فرض کنید سه تصویر را به یک اتاق انتساب داده‌اید. در حالت ویرایش، ابتدا این سه تصویر نمایش داده می‌شوند. بنابراین در لیست HotelRoomModel.HotelRoomImages وجود خواهند داشت. اکنون کاربر دو تصویر جدید دیگر را هم به این لیست اضافه می‌کند. در زمان ثبت، در متد AddHotelRoomImageAsync، بررسی نمی‌کنیم که این تصویر اضافه شده، جدید است یا خیر  و یا همان سه تصویر ابتدای کار نمایش فرم در حالت ویرایش هستند. به همین جهت رکوردها، تکراری ثبت می‌شوند.
برای رفع این مشکل می‌توان در متد AddHotelRoomImageAsync، جدید بودن یک تصویر را بر اساس RoomId آن بررسی کرد. اگر این RoomId مساوی صفر بود، یعنی تازه به لیست اضافه شده‌است و حاصل بارگذاری اولیه‌ی فرم ویرایش اطلاعات نیست:
    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages.Where(x => x.RoomId == 0))
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
در قسمت بعد، کدهای حذف اطلاعات اتاق‌ها و تصاویر مرتبط با هر کدام را نیز تکمیل خواهیم کرد.


یک نکته: متد AddHotelRoomImageAsync اضافی است!

چون از AutoMapper استفاده می‌کنیم، در ابتدای متد ثبت یک اتاق، کار نگاشت DTO، به موجودیت متناظر با آن انجام می‌شود:
public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
{
   var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
یعنی در اینجا چون خاصیت مجموعه‌ای HotelRoomImages موجود در HotelRoomDTO با نمونه‌ی مشابه آن در HotelRoom هم نام است، به صورت خودکار توسط AutoMapper به آن انتساب داده می‌شود و چون رابطه‌ی one-to-many در EF-Core تنظیم شده، همینقدر که hotelRoom حاصل، به همراه HotelRoomImages از پیش مقدار مقدار دهی شده‌است، به صورت خودکار آن‌ها را جزئی از اطلاعات همین اتاق ثبت می‌کند.
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شده‌اند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آن‌را به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی می‌کند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شده‌ی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-17.zip
مطالب
Blazor 5x - قسمت 26 - برنامه‌ی Blazor WASM - ایجاد و تنظیمات اولیه
در قسمت قبل، پایه‌ی Web API و سرویس‌های سمت سرور برنامه‌ی کلاینت Blazor WASM این سری را آماده کردیم. این برنامه‌ی سمت کلاینت، قرار است توسط عموم کاربران آن جهت رزرو کردن اتاق‌های هتل فرضی مثال این سری، مورد استفاده قرار گیرد. پیش از این نیز یک برنامه‌ی Blazor Server را تهیه کردیم که کار آن صرفا محدود است به مسائل مدیریتی هتل؛ مانند تعریف اتاق‌ها و امکانات رفاهی آن.


ایجاد یک پروژه‌ی جدید Blazor WASM

برای تکمیل پیاده سازی قسمت سمت کلاینت پروژه‌ی این سری، نیاز به یک پروژه‌ی جدید Blazor WASM را داریم که می‌توان آن‌را با اجرای دستور dotnet new blazorwasm  در یک پوشه‌ی خالی، ایجاد کرد. کدهای این پروژه را می‌توانید در پوشه‌ی HotelManagement\BlazorWasm\BlazorWasm.Client فایل پیوستی انتهای بحث مشاهده کنید.


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

شبیه به کاری که در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» انجام دادیم، در اینجا هم قصد افزودن یکسری کتابخانه‌ی جاوااسکریپتی و CSS ای را داریم که توسط LibMan آن‌ها را مدیریت خواهیم کرد.
- بنابراین در ابتدا به پوشه‌ی BlazorWasm.Client\wwwroot\css وارد شده و پوشه‌های پیش‌فرض bootstrap و open-iconic آن‌را حذف می‌کنیم؛ چون تحت مدیریت هیچ package manager ای نیستند و در این حالت، مدیریت به روز رسانی و یا بازیابی آن‌ها به صورت خودکار میسر نیست.
- سپس فایل wwwroot\css\app.css را هم ویرایش کرده و سطر زیر را از ابتدای آن حذف می‌کنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
- اکنون دستورات زیر را در ریشه‌ی پروژه‌ی WASM، اجرا می‌کنیم تا کتابخانه‌های مدنظر ما، تحت مدیریت libman، در پوشه‌ی wwwroot/lib نصب شوند:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap
libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
libman install jquery --provider unpkg --destination wwwroot/lib/jquery
libman install toastr --provider unpkg --destination wwwroot/lib/toastr
این دستورات همچنین فایل libman.json متناظری را نیز جهت اجرای دستور libman restore برای دفعات آتی، تولید می‌کند.

- بعد از نصب بسته‌های ذکر شده، فایل wwwroot\index.html را به صورت زیر به روز رسانی می‌کنیم تا به مسیرهای جدید بسته‌های CSS و JS نصب شده، اشاره کند:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <title>BlazorWasm.Client</title>
    <base href="/" />

    <link href="lib/toastr/build/toastr.min.css" rel="stylesheet" />
    <link
      href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css"
      rel="stylesheet"
    />
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorWasm.Client.styles.css" rel="stylesheet" />
  </head>

  <body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
      An unhandled error has occurred.
      <a href="" class="reload">Reload</a>
      <a class="dismiss">🗙</a>
    </div>

    <script src="lib/jquery/dist/jquery.min.js"></script>
    <script src="lib/toastr/build/toastr.min.js"></script>
    <script src="js/common.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
  </body>
</html>
مداخل فایل‌های css را در قسمت head و فایل‌های js را پیش از بسته شدن تگ body تعریف می‌کنیم. در اینجا نیازی به ذکر پوشه‌ی آغازین wwwroot نیست؛ چون base href تعریف شده، به این پوشه اشاره می‌کند.

- محتویات فایل wwwroot\css\app.css را هم به صورت زیر تغییر می‌دهیم تا یک spinner و شیوه نامه‌های نمایش تصاویر، به آن اضافه شوند:
.valid.modified:not([type="checkbox"]) {
  outline: 1px solid #26b050;
}

.invalid {
  outline: 1px solid red;
}

.validation-message {
  color: red;
}

#blazor-error-ui {
  background: lightyellow;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
}

#blazor-error-ui .dismiss {
  cursor: pointer;
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
}

.spinner {
  border: 16px solid silver !important;
  border-top: 16px solid #337ab7 !important;
  border-radius: 50% !important;
  width: 80px !important;
  height: 80px !important;
  animation: spin 700ms linear infinite !important;
  top: 50% !important;
  left: 50% !important;
  transform: translate(-50%, -50%);
  position: absolute !important;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
- همچنین فایل جدید wwwroot\js\common.js را که در قسمت 11 این سری ایجاد کردیم، به پروژه‌ی جاری نیز با محتوای زیر اضافه می‌کنیم تا سبب سهولت دسترسی به toastr شود:
window.ShowToastr = (type, message) => {
  if (type === "success") {
    toastr.success(message, "Operation Successful", { timeOut: 10000 });
  }
  if (type === "error") {
    toastr.error(message, "Operation Failed", { timeOut: 10000 });
  }
};

- در قسمت 11، در بخش «کاهش کدهای تکراری فراخوانی متدهای جاوا اسکریپتی با تعریف متدهای الحاقی» آن، کلاس JSRuntimeExtensions را تعریف کردیم که سبب کاهش تکرار کدهای استفاده از تابع ShowToastr می‌شود. این فایل‌را در پروژه‌ی BlazorServer.App\Utils\JSRuntimeExtensions.cs این سری نیز استفاده کردیم. یا می‌توان مجددا آن‌را به پروژه‌ی جاری کپی کرد؛ یا آن‌را در یک پروژه‌ی اشتراکی قرار داد. برای مثال اگر آن‌را به پوشه‌ی BlazorWasm.Client\Utils کپی کردیم، نیاز است فضای نام آن‌را اصلاح کرده و سپس آن‌را به انتهای فایل BlazorWasm.Client\_Imports.razor نیز اضافه کنیم تا در تمام کامپوننت‌های برنامه قابل استفاده شود:
@using BlazorWasm.Client.Utils


تغییر و ساده سازی منوی برنامه‌ی کلاینت

در برنامه‌ی کلاینت جاری دیگر نمی‌خواهیم منوی پیش‌فرض سمت چپ صفحه را شاهد باشیم. به همین جهت ابتدا فایل Shared\MainLayout.razor را به صورت زیر ساده می‌کنیم:
@inherits LayoutComponentBase

<NavMenu />
<div>
  @Body
</div>
سپس محتوای فایل Shared\NavMenu.razor را نیز حذف کرده و با تعاریف زیر جایگزین می‌کنیم:
<nav class="navbar navbar-expand-sm navbar-dark bg-dark p-0">
    <a class="navbar-brand mx-4" href="#">Navbar</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent"
            aria-expanded="false"
            aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse pr-2" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto"></ul>
        <ul class="my-0 navbar-nav">
            <li class="nav-item p-0">
                <NavLink class="nav-link" href="registration">
                    <span class="p-2">
                        Register
                    </span>
                </NavLink>
            </li>
            <li class="nav-item p-0">
                <NavLink class="nav-link" href="login">
                    <span class="p-2">
                        Login
                    </span>
                </NavLink>
            </li>
        </ul>
    </div>
</nav>
تا اینجا اگر برنامه‌ی سمت کلاینت را اجرا کنیم، شکل زیر را پیدا کرده که به همراه یک navbar افقی قرار گرفته‌ی در بالای صفحه است؛ به همراه دو لینک به قسمت‌های ثبت‌نام و لاگین:



تغییر محتوای صفحه‌ی آغازین برنامه


صفحه‌ی ابتدایی برنامه، یعنی کامپوننت Pages\Index.razor را نیز به صورت زیر تغییر می‌دهیم:
@page "/"

<form>
    <div class="row p-0 mx-0 mt-4">
        <div class="col-12 col-md-5  offset-md-1 pl-2  pr-2 pr-md-0">
            <div class="form-group">
                <label>Check In Date</label>
                <input type="text" class="form-control" />
            </div>
        </div>
        <div class="col-8 col-md-3 pl-2 pr-2">
            <div class="form-group">
                <label>No. of nights</label>
                <select class="form-control">
                    @for (var i = 1; i <= 10; i++)
                    {
                        <option value="@i">@i</option>
                    }
                </select>
            </div>
        </div>
        <div class="col-4 col-md-2 p-0 pr-2">
            <div class="form-group">
                <label>&nbsp;</label>
                <input type="submit" value="Go" class="btn btn-success btn-block" />
            </div>
        </div>
    </div>
</form>
در اینجا فرمی تعریف شده که تاریخ ورود و رزرو اتاقی را مشخص می‌کند؛ به همراه دراپ‌داونی برای انتخاب تعداد شب‌های اقامت مدنظر.


تعریف View Model رابط کاربری Pages\Index.razor

پس از تعریف محتوای ثابت برنامه، اکنون نوبت به پویا سازی آن است. به همین جهت نیاز است مدلی را برای صفحه‌ی آغازین برنامه تعریف کرد تا بتوان فرم آن‌را به این مدل متصل کرد. این مدل چون مختص به برنامه‌ی کلاینت است، آن‌را در پوشه‌ی جدید Models\ViewModels ایجاد می‌کنیم:
using System;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HomeVM
    {
        public DateTime StartDate { get; set; } = DateTime.Now;

        public DateTime EndDate { get; set; }

        public int NoOfNights { get; set; } = 1;
    }
}
در اینجا EndDate، یک خاصیت محاسباتی است که بر اساس تاریخ شروع و تعداد شب‌های انتخابی، قابل محاسبه‌است.
پس از این تعریف، بهتر است فضای نام آن‌را نیز به فایل BlazorWasm.Client\_Imports.razor افزود، تا کار با آن در کامپوننت‌های برنامه، ساده‌تر شود:
using BlazorWasm.Client.Models.ViewModels
اکنون می‌توان فرم Pages\Index.razor را به مدل فوق متصل کرد که شامل این تغییرات است:
- ابتدا فیلدی که ارائه کننده‌ی شیء ViewModel فرم است را تعریف می‌کنیم:
@code{
    HomeVM HomeModel = new HomeVM();
}
- سپس بجای یک form ساده، از EditForm اشاره کننده‌ی به این فیلد، استفاده خواهیم کرد:
<EditForm Model="HomeModel">
 // ...
</EditForm>
- در آخر بجای input معمولی، از کامپوننت InputDate متصل به HomeModel.StartDate :
<InputDate min="@DateTime.Now.ToString("yyyy-MM-dd")"
           @bind-Value="HomeModel.StartDate"
           type="text"
           class="form-control" />
و بجای select معمولی، از نمونه‌ی متصل شده‌ی به HomeModel.NoOfNights استفاده می‌کنیم:
<select @bind="HomeModel.NoOfNights">


تعریف Local Storage سمت کلاینت

در ادامه می‌خواهیم اگر کاربری زمان شروع رزرو اتاقی را به همراه تعداد شب مدنظر، انتخاب کرد، با کلیک بر روی دکمه‌ی Go، به یک صفحه‌ی مشاهده‌ی جزئیات منتقل شود. بنابراین نیاز داریم تا اطلاعات انتخابی کاربر را به نحوی ذخیره سازی کنیم. برای یک چنین سناریوی سمت کلاینتی، می‌توان از local storage استاندارد مرورگرها استفاده کرد که امکان کار آفلاین با برنامه را نیز فراهم می‌کند.
برای این منظور کتابخانه‌ای به نام Blazored.LocalStorage طراحی شده‌است که پس از نصب آن توسط دستور زیر:
dotnet add package Blazored.LocalStorage
نیاز است سرویس‌های آن‌را به سیستم تزریق وابستگی‌های برنامه اضافه کرد. در برنامه‌های Blazor Server، اینکار را در فایل Startup برنامه انجام می‌دادیم؛ اما در اینجا، سرویس‌ها در فایل Program.cs تعریف می‌شوند:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddBlazoredLocalStorage();
            // ...
        }
    }
}
پس از این تعاریف می‌توان از سرویس ILocalStorageService آن در کامپوننت‌های برنامه استفاده کرد. البته جهت سهولت استفاده‌ی از این سرویس بهتر است فضای نام آن‌را به فایل BlazorWasm.Client\_Imports.razor افزود:
@using Blazored.LocalStorage
اکنون برای استفاده از آن به کامپوننت Pages\Index.razor مراجعه کرده و سرویس‌های ILocalStorageService و IJSRuntime را به کامپوننت تزریق می‌کنیم:
@page "/"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime

<EditForm Model="HomeModel" OnValidSubmit="SaveInitialData">
همچنین متدی را هم برای مدیریت رویداد OnValidSubmit تعریف خواهیم کرد:
@code{
    HomeVM HomeModel = new HomeVM();

    private async Task SaveInitialData()
    {
        try
        {
            HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights);
            await LocalStorage.SetItemAsync("InitialRoomBookingInfo", HomeModel);
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
در اینجا با استفاده از متد SetItemAsync و ذکر یک کلید دلخواه، اطلاعات مدل فرم را در local storage مرورگر ذخیره کرده‌ایم. همچنین اگر خطایی هم رخ دهد توسط ToastrError نمایش داده خواهد شد.
برای مثال اگر تاریخ و عددی را انتخاب کنیم، نتیجه‌ی حاصل از کلیک بر روی دکمه‌ی Go را می‌توان در قسمت Local storage مرورگر جاری مشاهده کرد:


البته با توجه به اینکه می‌خواهیم از کلید InitialRoomBookingInfo در سایر کامپوننت‌های برنامه نیز استفاده کنیم، بهتر است آن‌را به یک پروژه‌ی مشترک مانند BlazorServer.Common که پیشتر نام نقش‌هایی مانند Admin را در آن تعریف کردیم، منتقل کنیم:
namespace BlazorServer.Common
{
    public static class ConstantKeys
    {
        public const string LocalInitialBooking = "InitialRoomBookingInfo";
    }
}
سپس باید ارجاعی به آن پروژه را افزوده:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.Common\BlazorServer.Common.csproj" />
  </ItemGroup>
</Project>
همچنین فضای نام آن‌را نیز به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم:
@using BlazorServer.Common
اکنون می‌توان از کلید ثابت تعریف شده‌ی مشترک، استفاده کرد:
await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);

در آخر قصد داریم با کلیک بر روی Go، به یک صفحه‌ی جدید مانند نمایش لیست اتاق‌ها هدایت شویم. به همین جهت کامپوننت جدید Pages\HotelRooms\HotelRooms.razor را ایجاد می‌کنیم:
@page "/hotel/rooms"

<h3>HotelRooms</h3>

@code {

}
سپس در کامپوننت Pages\Index.razor با استفاده از سرویس NavigationManager، کار هدایت خودکار کاربر را به این کامپوننت جدید انجام خواهیم داد:
@page "/"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager


@code{
    HomeVM HomeModel = new HomeVM();

    private async Task SaveInitialData()
    {
        try
        {
            HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights);
            await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);
            NavigationManager.NavigateTo("hotel/rooms");
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-26.zip
مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت دوم
در مطلب «طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول» با ساختار کلی یک پروژه‌ی افزونه‌ی پذیر ASP.NET MVC آشنا شدیم. پس از راه اندازی آن و مدتی کار کردن با این نوع پروژه‌ها، این سؤال پیش خواهد آمد که ... خوب، اگر هر افزونه تصاویر یا فایل‌های CSS و JS اختصاصی خودش را بخواهد داشته باشد، چطور؟ موارد عمومی مانند بوت استرپ و جی‌کوئری را می‌توان در پروژه‌ی پایه قرار داد تا تمام افزونه‌ها به صورت یکسانی از آن‌ها استفاده کنند، اما هدف، ماژولار شدن برنامه است و جدا کردن فایل‌های ویژه‌ی هر پروژه، از پروژ‌ه‌ای دیگر و همچنین بالا بردن سهولت کار تیمی، با شکستن اجزای یک پروژه به صورت افزونه‌هایی مختلف، بین اعضای یک تیم. در این قسمت نحوه‌ی مدفون سازی انواع فایل‌های استاتیک افزونه‌ها را درون فایل‌های DLL آن‌ها بررسی خواهیم کرد. به این ترتیب دیگر نیازی به ارائه‌ی مجزای آن‌ها و یا کپی کردن آن‌ها در پوشه‌های پروژه‌ی اصلی نخواهد بود.


مدفون سازی فایل‌های CSS و JS هر افزونه درون فایل DLL آن

به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاس‌های عمومی و مشترک بین افزونه‌ها استفاده خواهیم کرد. برای مثال قصد نداریم کلاس‌های سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونه‌ای جدید کپی کنیم. کتابخانه‌ی Common، امکان استفاده‌ی مجدد از یک سری کدهای تکراری را در بین افزونه‌ها میسر می‌کند.
این پروژه برای کامپایل شدن نیاز به بسته‌ی نیوگت ذیل دارد:
 PM> install-package Microsoft.AspNet.Web.Optimization
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.
پس از این مقدمات، کلاس ذیل را به این پروژه‌ی class library جدید اضافه کنید:
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web.Optimization;
 
namespace MvcPluginMasterApp.Common.WebToolkit
{
    public class EmbeddedResourceTransform : IBundleTransform
    {
        private readonly IList<string> _resourceFiles;
        private readonly string _contentType;
        private readonly Assembly _assembly;
 
        public EmbeddedResourceTransform(IList<string> resourceFiles, string contentType, Assembly assembly)
        {
            _resourceFiles = resourceFiles;
            _contentType = contentType;
            _assembly = assembly;
        }
 
        public void Process(BundleContext context, BundleResponse response)
        {
            var result = new StringBuilder();
 
            foreach (var resource in _resourceFiles)
            {
                using (var stream = _assembly.GetManifestResourceStream(resource))
                {
                    if (stream == null)
                    {
                        throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", resource, _assembly.FullName));
                    }
 
                    using (var reader = new StreamReader(stream))
                    {
                        result.Append(reader.ReadToEnd());
                    }
                }
            }
 
            response.ContentType = _contentType;
            response.Content = result.ToString();
        }
    }
}
اگر با سیستم bundling & minification کار کرده باشید، با تعاریفی مانند ("new Bundle("~/Plugin1/Scripts آشنا هستید. سازنده‌ی کلاس Bundle، پارامتر دومی را نیز می‌پذیرد که از نوع IBundleTransform است. با پیاده سازی اینترفیس IBundleTransform می‌توان محل ارائه‌ی فایل‌های استاتیک CSS و JS را بجای فایل سیستم متداول و پیش فرض، به منابع مدفون شده‌ی در اسمبلی جاری هدایت و تنظیم کرد.
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایل‌ها و منابع مدفون شده گشته و سپس محتوای آن‌ها را بازگشت می‌دهد.
اکنون برای استفاده‌ی از آن، به پروژه‌ی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژه‌ی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آن‌را به نحو ذیل تکمیل کنید:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return null;
        }
 
        public MenuItem GetMenuItem(RequestContext requestContext)
        {
            return new MenuItem
            {
                Name = "Plugin 1",
                Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" })
            };
        }
 
        public void RegisterBundles(BundleCollection bundles)
        {
            var executingAssembly = Assembly.GetExecutingAssembly();
            // Mostly the default namespace and assembly name are the same
            var assemblyNameSpace = executingAssembly.GetName().Name;
            var scriptsBundle = new Bundle("~/Plugin1/Scripts",
                new EmbeddedResourceTransform(new List<string>
                {
                    assemblyNameSpace + ".Scripts.test1.js"
                }, "application/javascript", executingAssembly));
            if (!HttpContext.Current.IsDebuggingEnabled)
            {
                scriptsBundle.Transforms.Add(new JsMinify());
            }
            bundles.Add(scriptsBundle);
            var cssBundle = new Bundle("~/Plugin1/Content",
                new EmbeddedResourceTransform(new List<string>
                {
                    assemblyNameSpace + ".Content.test1.css"
                }, "text/css", executingAssembly));
            if (!HttpContext.Current.IsDebuggingEnabled)
            {
                cssBundle.Transforms.Add(new CssMinify());
            }
            bundles.Add(cssBundle);
            BundleTable.EnableOptimizations = true;
        }
 
        public void RegisterRoutes(RouteCollection routes)
        {
        }
 
        public void RegisterServices(IContainer container)
        {
        }
    }
}
در اینجا نحوه‌ی کار با کلاس سفارشی EmbeddedResourceTransform را مشاهده می‌کنید. ابتدا فایل‌های js و سپس فایل‌های css برنامه به سیستم Bundling برنامه اضافه شده‌اند.
این فایل‌ها به صورت ذیل در پروژه تعریف گردیده‌اند:


همانطور که مشاهده می‌کنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آن‌ها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.

یک نکته‌ی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نام‌هایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره می‌شوند:



مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن

مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using System.Web.Routing;
 
namespace MvcPluginMasterApp.Common.WebToolkit
{
    public class EmbeddedResourceRouteHandler : IRouteHandler
    {
        private readonly Assembly _assembly;
        private readonly string _resourcePath;
        private readonly TimeSpan _cacheDuration;
 
        public EmbeddedResourceRouteHandler(Assembly assembly, string resourcePath, TimeSpan cacheDuration)
        {
            _assembly = assembly;
            _resourcePath = resourcePath;
            _cacheDuration = cacheDuration;
        }
 
        IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
        {
            return new EmbeddedResourceHttpHandler(requestContext.RouteData, _assembly, _resourcePath, _cacheDuration);
        }
    }
 
    public class EmbeddedResourceHttpHandler : IHttpHandler
    {
        private readonly RouteData _routeData;
        private readonly Assembly _assembly;
        private readonly string _resourcePath;
        private readonly TimeSpan _cacheDuration;
 
        public EmbeddedResourceHttpHandler(
            RouteData routeData, Assembly assembly, string resourcePath, TimeSpan cacheDuration)
        {
            _routeData = routeData;
            _assembly = assembly;
            _resourcePath = resourcePath;
            _cacheDuration = cacheDuration;
        }
 
        public bool IsReusable
        {
            get { return false; }
        }
 
        public void ProcessRequest(HttpContext context)
        {
            var routeDataValues = _routeData.Values;
            var fileName = routeDataValues["file"].ToString();
            var fileExtension = routeDataValues["extension"].ToString();
 
            var manifestResourceName = string.Format("{0}.{1}.{2}", _resourcePath, fileName, fileExtension);
            var stream = _assembly.GetManifestResourceStream(manifestResourceName);
            if (stream == null)
            {
                throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", manifestResourceName, _assembly.FullName));
            }
 
            context.Response.Clear();
            context.Response.ContentType = "application/octet-stream";
            cacheIt(context.Response, _cacheDuration);
            stream.CopyTo(context.Response.OutputStream);
        }
 
        private static void cacheIt(HttpResponse response, TimeSpan duration)
        {
            var cache = response.Cache;
 
            var maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
            if (maxAgeField != null) maxAgeField.SetValue(cache, duration);
 
            cache.SetCacheability(HttpCacheability.Public);
            cache.SetExpires(DateTime.Now.Add(duration));
            cache.SetMaxAge(duration);
            cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
        }
    }
}
تصاویر پروژه‌ی افزونه نیز به صورت embedded resource در اسمبلی آن قرار خواهند گرفت. به همین جهت باید سیستم مسیریابی را پس درخواست رسیده‌ی جهت نمایش تصاویر، به منابع ذخیره شده‌ی در اسمبلی آن هدایت نمود. اینکار را با پیاده سازی یک IRouteHandler سفارشی، می‌توان به نحو فوق مدیریت کرد.
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت می‌دهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شده‌است و هدرهای خاص caching را به صورت خودکار اضافه می‌کند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه می‌کند.

اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    { 
        public void RegisterRoutes(RouteCollection routes)
        {
            //todo: add custom routes.
 
            var assembly = Assembly.GetExecutingAssembly();
            // Mostly the default namespace and assembly name are the same
            var nameSpace = assembly.GetName().Name;
            var resourcePath = string.Format("{0}.Images", nameSpace);
 
            routes.Insert(0,
                new Route("NewsArea/Images/{file}.{extension}",
                    new RouteValueDictionary(new { }),
                    new RouteValueDictionary(new { extension = "png|jpg" }),
                    new EmbeddedResourceRouteHandler(assembly, resourcePath, cacheDuration: TimeSpan.FromDays(30))
                ));
        } 
    }
}
در مسیریابی تعریف شده، تمام درخواست‌های رسیده‌ی به مسیر NewsArea/Images به EmbeddedResourceRouteHandler هدایت می‌شوند.
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی می‌شوند از نوع png یا jpg تعریف شده‌اند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شده‌است.


استفاده‌ی نهایی از تنظیمات فوق در یک View افزونه

پس از اینکه تصاویر و فایل‌های css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آن‌ها را نیز مشخص نمودیم، اکنون نوبت به استفاده‌ی از آن‌ها در یک View است:
@{
    ViewBag.Title = "From Plugin 1";
}
@Styles.Render("~/Plugin1/Content")
 
<h2>@ViewBag.Message</h2>
 
<div class="row">
    Embedded image:
    <img src="@Url.Content("~/NewsArea/Images/chart.png")" alt="clock" />
</div>
 
@section scripts
{
    @Scripts.Render("~/Plugin1/Scripts")
}
در اینجا نحوه‌ی تعریف فایل‌های CSS و JS ارائه شده‌ی توسط سیستم Bundling را مشاهده می‌کنید.
همچنین مسیر تصویر مشخص شده‌ی در آن، اینبار یک NewsArea اضافه‌تر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفته‌است اما می‌خواهیم این درخواست‌ها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.

اینبار اگر برنامه را اجرا کنیم، می‌توان به سه نکته در آن دقت داشت:


الف) alert اجرا شده از فایل js مدفون شده خوانده شده‌است.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شده‌است.
ج) تصویر نمایش داده شده، همان تصویر مدفون شده‌ی در فایل DLL برنامه است.
و هیچکدام از این فایل‌ها، به پوشه‌های پروژه‌ی اصلی برنامه، کپی نشده‌اند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
 MvcPluginMasterApp-Part2.zip
مطالب
PowerShell 7.x - قسمت یازدهم - یک مثال
به عنوان یک تمرین میخواهیم یک پروژه گیت ساده را به صورت زیر visualise کنیم: 


برای تولید چنین نموداری از میتوانیم Mermaid استفاده کنیم. یکی از انواع دیاگرام‌هایی را که پشتیبانی میکند، Gitgraph میباشد. دیاگرام‌ها در Mermaid با کمک یک DSL ساخته میشوند. سینکس آن نیز خیلی ساده است؛ ابتدا نوع دیاگرامی را که میخواهیم ترسیم کنیم، تعیین میکنیم و سپس محتویات را براساس نوع دیاگرام، تعیین میکنیم. به عنوان مثال برای Gitgraph سینتکس آن به این صورت است: 

gitGraph
   commit
   commit
   branch develop
   checkout develop
   commit
   commit
   checkout main
   merge develop
   commit
   commit

در ساختار فوق ابتدا دو کامیت بر روی برنچ اصلی (main) انجام شده‌است؛ سپس یک برنچ جدید را با نام develop، ایجاده کرده‌ایم و بلافاصله به آن checkout کرده‌ایم. در ادامه تعدادی کامیت را بر روی این برنچ انجام داده و در نهایت برنچ موردنظر را بر روی main، مرج کرده‌ایم. در نهایت نیز دو کامیت دیگر را بر روی main ایجاد کرده‌ایم. تعریف فوق، منجر به ساخت چنین نموداری خواهد شد: 


از ادیتور آنلاین Mermaid نیز میتوانید برای تست سینکس استفاده کنید. در ادامه میخواهیم با کمک PowerShell، از روی یک پروژه‌ی گیت، DSL موردنیاز برای ساخت دیاگرام را ایجاد کنیم. برای اینکار ابتدا توسط تابع زیر یک پروژه‌ی گیت را با تعدادی فایل نمونه ایجاد خواهیم کرد: 

Function New-RandomRepo {
    Function RandomFiles($branch = "main") {
        1..3 | ForEach-Object {
            New-Item -ItemType File -Name "file_$($_).txt"
            Set-Content -Path "file_$($_).txt" -Value "This is file $($_) on branch $branch"
            git add .
            git commit -m "Commit $($_) on branch $branch"
        }
    }
    Set-Location ~/Desktop
    New-Item -ItemType Directory "random_git_repo"
    Set-Location "random_git_repo"
    git init -b main
    Write-Output "This is the main branch" | Set-Content -Path "main.txt"
    git add .
    git commit -m "Initial commit"
    1..3 | ForEach-Object { 
        git checkout -b "branch_$($_)"
        RandomFiles "branch_$($_)"
        git checkout main
    }
}

در ادامه تابع New-GitRepoDiagram را برای تولید ساختار مورد نیاز نوشته‌ایم: 

Function New-GitRepoDiagram {
    $commitIds = @()
    $branches = git branch | ForEach-Object {
        $default = $false
        $activeBranch = git symbolic-ref --short HEAD
        $currentBranch = ($_.Replace("* ", " ")).Trim()
        if ($currentBranch -eq $activeBranch) {
            $default = $true
        }
        @{
            name         = $currentBranch
            isMainBranch = $default
        } | ConvertTo-Json
    } | ConvertFrom-Json
    
    $defaultBranch = $branches | Where-Object { $_.isMainBranch -eq $true } | Select-Object -ExpandProperty name
    $mermaidFile = "%%{init: { 'gitGraph': {'mainBranchName': '$defaultBranch' } } }%%" + [Environment]::NewLine
    $mermaidFile += 'gitGraph' + [Environment]::NewLine

    foreach ($branch in ($branches | Sort-Object -Property isMainBranch -Descending)) {
        $name = $branch.name
        $notIncludeTheMainCommits = $name -ne $defaultBranch ? "--not $(git merge-base $defaultBranch $name)" : ""
        $notIncludeTheMainCommits
        $logs = git log --pretty=format:'{"commit": "%h", "author": "%an", "message": "%s"}' --reverse $name | ConvertFrom-Json
        if ($name -ne $defaultBranch) {
            $mermaidFile += '   branch "$name"'.Replace('$name', $name) + [Environment]::NewLine
        }
        foreach ($log in $logs) {
            $commit = $log.commit
            if ($commitIds -contains $commit) {
                continue
            }
            $commitToAdd = '   commit id: "$commit"'.Replace('$commit', $commit) + [Environment]::NewLine
            $mermaidFile += $commitToAdd
            $commitIds += $commit
        }
        if ($name -ne $defaultBranch) {
            $mermaidFile += '   checkout main' + [Environment]::NewLine
        }
    }
    Write-Host $mermaidFile
}

توضیحات:

  • در تابع فوق، ابتدا یک آرایه‌ی خالی برای ذخیره‌ی کامیت آی‌دی‌ها، اضافه شده؛ از این آرایه برای جلوگیری از اضافه شدن کامیت تکراری در هر برنچ استفاده شده‌است.
  • سپس با کمک دستور git branch، لیست تمام برنچ‌ها، دریافت شده‌است (همانطور که در قسمت قبل بررسی شد +).
  • برای هر برنچ، تابع تعیین میکند که آیا برنچ جاری است یا خیر. اینکار با کمک دستور git symbolic-ref انجام شده‌است.
  • در ادامه متغیر mermaidFile$ برای ایجاد یک گراف جدید Git مقداردهی میشود. در اینجا نام برنچ اصلی برابر با نام برنچ پیش‌فرض، تنظیم می‌شود.
  • سپس لیست کامیت‌های هر برنچ را با کمک دستور git log (همان سینتکسی که در قسمت‌های قبل بررسی شد +) استخراج میکنیم.
  • و در نهایت به ازای هر کامیت، یک commit id تولید کرده‌ایم.

با فراخوانی تابع فوق، اینچنین ساختاری برایمان تولید خواهد شد: 

%%{init: { 'gitGraph': {'mainBranchName': 'main' } } }%%
gitGraph
   commit id: "765100f"
   branch "branch_1"
   commit id: "c88c441"
   commit id: "44149d9"
   commit id: "a660fe3"
   checkout main
   branch "branch_2"
   commit id: "2dcb572"
   commit id: "b043ad1"
   commit id: "92cafc0"
   checkout main
   branch "branch_3"
   commit id: "559e381"
   commit id: "f72957f"
   commit id: "c066e72"
   checkout main

خروجی فوق دقیقاً دیاگرامی است که در ابتدای مطلب نشان داده شد:

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

قسمت قبل به IIS7‌ اختصاص داشت که شاید برای خیلی‌ها کاربرد نداشته باشد خصوصا اینکه برنامه نویس‌ها ترجیح می‌دهند به روش‌هایی روی بیاورند که کمتر نیاز به دخالت مدیر سرور داشته باشد؛ یا زمانیکه سایت شما بر روی یک هاست اینترنتی قرار گرفته است عملا شاید دسترسی خاصی به تنظیمات IIS نداشته باشید (مگر اینکه یک هاست اختصاصی را تهیه کنید).
برای IIS6 و ماقبل از آن و حتی بعد از آن!، حداقل دو روش برای کش کردن اطلاعات استاتیک وجود دارد:

الف) استفاده از web resources معرفی شده در ASP.Net 2.0 به بعد
در مورد نحوه‌ی تعریف و بکارگیری web resources می‌توان به مقاله "تبدیل پلاگین‌های jQuery‌ به کنترل‌های ASP.Net" رجوع کرد.


همانطور که در شکل فوق نیز ملاحظه می‌کنید، هدر مربوط به مدت زمان منقضی شدن کش سمت کلاینت یک web resource توسط موتور ASP.Net به صورت خودکار به سال 2010 تنظیم شده است و این مقدار خالی نیست.

ب) افزودن این هدر به صورت دستی

برای این منظور باید در نحوه‌ی ارائه فایل‌های استاتیک دخالت کنیم و این‌کار را با استفاده از یک generic handler می‌توان انجام داد.


کد این generic handler می‌تواند به صورت زیر باشد:

using System;
using System.IO;
using System.Web;
using System.Web.Services;
using System.Reflection;

namespace test1
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class cache : IHttpHandler
{

private static void cacheIt(TimeSpan duration)
{
HttpCachePolicy cache = HttpContext.Current.Response.Cache;

FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
maxAgeField.SetValue(cache, duration);

cache.SetCacheability(HttpCacheability.Public);
cache.SetExpires(DateTime.Now.Add(duration));
cache.SetMaxAge(duration);
cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
}

public void ProcessRequest(HttpContext context)
{
string file = context.Request.QueryString["file"];
if (string.IsNullOrEmpty(file))
{
return;
}

string contetType = context.Request.QueryString["contetType"];
if (string.IsNullOrEmpty(contetType))
{
return;
}

context.Response.Write(File.ReadAllText(context.Server.MapPath(file)));

//Set the content type
context.Response.ContentType = contetType;

// Cache the resource for 30 Days
cacheIt(TimeSpan.FromDays(30));
}

public bool IsReusable
{
get
{
return false;
}
}
}
}
توضیحات:
این generic handler دو کوئری استرینگ را دریافت می‌کند؛ file جهت دریافت نام فایل و contetType جهت مشخص سازی نوع محتوایی که باید سرو شود؛ مثلا جاوا اسکریپت یا استایل شیت و امثال آن. سپس زمانیکه محتوا را Response.Write می‌کند، هدر مربوط به کش شدن آن‌را نیز به 30 روز تنظیم می‌نماید.
تابع مربوط به کش کردن اطلاعات از مقاله ASP.NET Ajax Under-the-hood Secrets استخراج شد.

روش استفاده در مورد فایل‌های CSS
بجای تعریف یک فایل CSS در صفحه، به صورت استاندارد، اکنون تعریف متداول را به صورت زیر اصلاح کنید:

<link type="text/css" href="cache.ashx?v=1&file=site.css&contetType=text/css" rel="Stylesheet" />
هر زمانیکه که فایل site.css درخواست می‌شود، باید از فیلتر ما عبور کند و سپس ارائه گردد. در این حین، هدر مربوط به مدت زمان کش شدن سمت کلاینت به آن اضافه می‌شود. از کوئری استرینگ مربوط v هم جهت به روز رسانی‌های بعدی استفاده می‌شود تا اگر تغییری را اعمال کردیم، کلاینت حتما با توجه به آدرس جدید، محتویات جدید را یکبار دیگر دریافت کند. (مرورگر آدرس‌های مشابه را در صورتیکه هدر مربوط به کش شدن آن‌ها تنظیم شده باشد، از کش خواهد خواند و کاری به آخرین تغییرات شما در سرور ندارد)

روش استفاده در مورد فایل‌های JS
<script type="text/javascript" src="cache.ashx?v=1&file=js/jquery-1.3.2.min.js&contetType=application/x-javascript"></script>
اکنون اگر سایت را مجددا با افزونه YSlow بررسی کنیم، می‌توان این هدر جدید را مشاهده کرد:



مطالب
ایجاد سیستم وضعیت آب و هوا مانند گوگل (بخش اول)
در این آموزش قصد دارم چگونگی ایجاد یک سیستم اعلام وضعیت آب و هوا را مشابه آنچه که در سایت گوگل می‌بینید برای شما توضیح دهم. باید توجه داشت من این آموزش را با  ASP.NET MVC نوشتم ولی شما می‌توانید با اندک تغییراتی در کدها، آنرا در ASP.NET وب فرمز نیز استفاده کنید. برای گرفتن آب و هوای هر شهر از Rss‌های اعلام وضعیت آب و هوای یاهو استفاده می‌کنم و توضیح خواهم داد که چگونه با Rss آن کار کنید.
Rss آب و هوای هر شهر در یاهو به صورت یک لینک یکتا می‌باشد؛ به شکل زیر :

http://weather.yahooapis.com/forecastrss?w=WOEID&u=c
حال می‌خواهم کوئری استرینگ‌های این لینک را برای شما توضیح دهم. هر شهری بر روی کره‌ی زمین یک WOEID یکتا و منحصر بفرد دارد که شما به پارامتر w عدد WOEID شهر موردنظر خود را می‌دهید. بعد از مقداردهی پارامتر w، وقتی این لینک را در آدرس بار مرورگر خود می‌زنید، RSS مربوط به آب و هوای آن شهر را به شما می‌دهد. مثلا WOEID تهران عدد 28350859 می‌باشد.
و این لینک http://weather.yahooapis.com/forecastrss?w=28350859&u=c اطلاعات آب و هوای تهران را در قالب یک RSS به شما نمایش خواهد داد.

خوب، حالا پارامتر دوم یعنی پارامتر u چکاری را انجام می‌دهد؟
* چنانچه مقدار پارامتر u برابر c باشد، یعنی شما دمای آب و هوای شهر مد نظر را بر اساس سانتیگراد می‌خواهید.
* اگر مقدار پارامتر u برابر f باشد، یعنی شما دمای آب و هوای آن شهر مورد نظر را بر اساس فارنهایت می‌خواهید.

برای گرفتن WOEID شهر‌ها هم به این سایت بروید http://woeid.rosselliot.co.nz و اسم هر شهری که می‌خواهید بزنید تا WOEID را به شما نمایش دهد.

در این مثال من از یک DropDown استفاده کردم که کاربر با انتخاب هر شهر از  DropDown، آب و هوای آن شهر را مشاهده می‌کند.
Action مربوط به صفحه‌ی Index به صورت زیر می‌باشد :
[HttpGet]
        public ActionResult Index()
        {
           ViewBag.ProvinceList = _RPosition.Positions;
            ShowWeatherProvince(8);
            return View();
        }
در اینجا من لیست شهر‌ها را از جدول می‌خوانم که البته این جدول را چون بخش مهمی نبود و فقط شامل ID و نام شهر‌ها بود در فایل ضمیمه قرار ندادم و نام شهر‌ها و ID آنها را بر عهده‌ی خودتان گذاشتم.
حال تابعی را که آب و هوای مربوط به هر شهر را نمایش می‌دهد، به شرح زیر است:
public ActionResult ShowWeatherProvince(int dpProvince)
        {
            XDocument rssXml=null;
            CountryName CountryName = new CountryName();
            if (dpProvince != 0)
            {
                switch (dpProvince)
                {
                    case 1:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345768&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Sharqhi" };
                            break;
                        }
                    case 2:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345767&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Azarbayejan-e Qarbi" };
                            break;
                        }
                    case 3:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254335&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Ardebil" };
                            break;
                        }
                    case 4:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Alborz" };
                            break;
                        }
                    case 5:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345787&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Esfahan" };
                            break;
                        }
                    case 6:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345775&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Ilam" };
                            break;
                        }
                    case 7:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254463&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Bushehr" };
                            break;
                        }
                    case 8:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=28350859&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Tehran" };
                            break;
                        }
                    case 9:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345769&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Chahar Mahall va Bakhtiari" };
                            break;
                        }
                    case 10:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=56189824&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Razavi Khorasan" };
                            break;
                        }
                    case 11:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Shomali Khorasan" };
                            break;
                        }
                    case 12:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345789&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Jonubi Khorasan" };
                            break;
                        }
                    case 13:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345778&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Khuzestan" };
                            break;
                        }
                    case 14:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255311&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Zanjan" };
                            break;
                        }
                    case 15:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345784&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Semnan" };
                            break;
                        }
                    case 16:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345770&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Sistan va Baluchestan" };
                            break;
                        }
                    case 17:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345772&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Fars" };
                            break;
                        }
                    case 18:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070200&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Qazvin" };
                            break;
                        }
                    case 19:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2255062&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Qom" };
                            break;
                        }
                    case 20:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345779&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Kordestan" };
                            break;
                        }
                    case 21:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254796&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Kerman" };
                            break;
                        }
                    case 22:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254797&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Kermanshah" };
                            break;
                        }
                    case 23:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345771&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Kohgiluyeh va Buyer Ahmad" };
                            break;
                        }
                    case 24:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=20070201&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Golestan" };
                            break;
                        }
                    case 25:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345773&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Gilan" };
                            break;
                        }
                    case 26:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345782&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Lorestan" };
                            break;
                        }
                    case 27:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345783&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Markazi" };
                            break;
                        }
                    case 28:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345780&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Mazandaran" };
                            break;
                        }
                    case 29:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2254664&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Hamedan" };
                            break;
                        }
                    case 30:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2345776&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Hormozgan" };
                            break;
                        }
                    case 31:
                        {
                            rssXml = XDocument.Load("http://weather.yahooapis.com/forecastrss?w=2253355&u=c");
                            CountryName = new CountryName() { Country = "Iran", City = "Yazd" };
                            break;
                        }
                }
                ViewBag.Location = CountryName;
                XNamespace yWeatherNS = "http://xml.weather.yahoo.com/ns/rss/1.0";
                List<YahooWeatherRssItem> WeatherList = new List<YahooWeatherRssItem>();
                for (int i = 0; i < 4; i++)
                {
                    YahooWeatherRssItem YahooWeatherRssItem = new YahooWeatherRssItem()
                    {
                        Code = Convert.ToInt32(rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("code").Value),
                        Day = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("day").Value,
                        Low = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("low").Value,
                        High = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("high").Value,
                        Text = rssXml.Descendants("item").Elements(yWeatherNS + "forecast").ElementAt(i).Attribute("text").Value,
                    };

                    WeatherList.Add(YahooWeatherRssItem);
                }
                ViewBag.FeedList = WeatherList;
            }

          
                return PartialView("_Weather");
           
        }
قسمت SwitchCase، مقدار و Value مربوط به هر آیتم DropDown را که شامل یک اسم شهر است، میگیرد و RSS مربوط به آن شهر را بر می‌گرداند.
حالا کد مربوط به خواندن فایل Rss را برایتان توضیح می‌دهم : حلقه‌ی for 0  تا 4 (که در کد بالا مشاهده می‌کنید)یعنی اطلاعات 4 روز آینده را برایم برگردان.
من تگ‌های Code ، Day ، Low ، High و text فایل RSS را در این حلقه For می‌خوانم که البته مقادیر این 4 روز را در لیستی اضافه می‌کنم که نوع این لیست هم از نوع YahooWeatherRssItem می‌باشد. من این کلاس را در فایل ضمیمه قرار دادم. اکنون هر کدام از این تگ‌ها را برایتان توضیح می‌دهم:

code : هر آب و هوا کدی دارد .مثلا آب و هوای نیمه ابری یک کد ، آب و هوای آفتابی کدی دیگر و ...
Low: حداقل دمای آن روز را به ما می‌دهد .
High: حداکثر دمای آن روز را به می‌دهد .
day: نام روز از هفته را بر می‌گرداند مثلا شنبه ، یکشنبه و ....
text: که توضیحاتی می‌دهد مثلا اگر هوا آفتابی باشد مقدار sunny را بر می‌گرداند و ...


خوب، تا اینجا ما Rss مربوط به هر شهر را خواندیم حالا در قسمت Design باید چکار کنیم .
کدهای html صفحه‌ی Index ما شامل کدهای زیر است :
@{

    ViewBag.Title = "Weather";
}

<link href="~/Content/User/Weather/Weather.css" rel="stylesheet" />
@section scripts{
    <script src="@Url.Content("~/Scripts/jquery-1.6.2.min.js")" type="text/javascript"></script>
        <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
       <script type="text/javascript">
           $("#dpProvince").change(function () {
               $(this).parents("form").submit();
           });
    </script>
}
<h2>Weather</h2>
<div id="Progress">
    <img src="~/Images/User/Other/ajax-loader.gif" />
</div>
<div id="BoxContent"> @Html.Partial("_Weather")</div>

  @using (Ajax.BeginForm(actionName: "ShowWeatherProvince", ajaxOptions: new AjaxOptions { UpdateTargetId = "BoxContent", LoadingElementId = "Progress", InsertionMode = InsertionMode.Replace }))
                {
<div style="padding-top:15px;">
        <div style="float:left; width:133px; ">Select Your Province</div>
        <div style="float:left">   @Html.DropDownList("dpProvince", new SelectList(ViewBag.ProvinceList, "Id", "Name"),"Select Your Province", new { @class = "webUserDropDown", @style = "width:172px" })</div>
    </div>
  }
و کدهای _Weather که Partial است به صورت زیر است:
@{
    List<Weather.YahooWeatherRssItem> Feeds = ViewBag.FeedList;
}
<div>
    @{
        HtmlString StartTable = new HtmlString("<table class='WeatherTable' cellspacing='0' cellpadding='0'><tbody><tr>");
        HtmlString EndTable = new HtmlString("</tr></tbody></table>");
        HtmlString StartTD = new HtmlString("<td>");
        HtmlString EndTD = new HtmlString("</td>");
    }
    <div style="width: 300px;">
        @{
            @StartTable
            foreach (var item in Feeds)
            {
            @StartTD
            <div>@item.Day</div>
            <div>
                @{
                string FileName = "";
                switch (item.Code)
                {
                    case 0: { FileName = "/Images/User/Weather/Tornado.png"; break; }
                    case 1: { FileName = "/Images/User/Weather/storm2.gif"; break; }
                    case 2: { FileName = "/Images/User/Weather/storm2.gif"; break; }
                    case 3: { FileName = "/Images/User/Weather/storm2.gif"; break; }
                    case 4: { FileName = "/Images/User/Weather/15.gif"; break; }
                    case 5: { FileName = "/Images/User/Weather/29.gif"; break; }
                    case 6: { FileName = "/Images/User/Weather/29.gif"; break; }
                    case 7: { FileName = "/Images/User/Weather/29.gif"; break; }
                    case 8: { FileName = "/Images/User/Weather/26.gif"; break; }
                    case 9: { FileName = "/Images/User/Weather/drizzle.png"; break; }
                    case 10: { FileName = "/Images/User/Weather/26.gif"; break; }
                    case 11: { FileName = "/Images/User/Weather/18.gif"; break; }
                    case 12: { FileName = "/Images/User/Weather/18.gif"; break; }
                    case 13: { FileName = "/Images/User/Weather/19.gif"; break; }
                    case 14: { FileName = "/Images/User/Weather/19.gif"; break; }
                    case 15: { FileName = "/Images/User/Weather/19.gif"; break; }
                    case 16: { FileName = "/Images/User/Weather/22.gif"; break; }
                    case 17: { FileName = "/Images/User/Weather/Hail.png"; break; }
                    case 18: { FileName = "/Images/User/Weather/25.gif"; break; }
                    case 19: { FileName = "/Images/User/Weather/dust.png"; break; }
                    case 20: { FileName = "/Images/User/Weather/fog_icon.png"; break; }
                    case 21: { FileName = "/Images/User/Weather/hazy_icon.png"; break; }
                    case 22: { FileName = "/Images/User/Weather/2017737395.png"; break; }
                    case 23: { FileName = "/Images/User/Weather/32.gif"; break; }
                    case 24: { FileName = "/Images/User/Weather/32.gif"; break; }
                    case 25: { FileName = "/Images/User/Weather/31.gif"; break; }
                    case 26: { FileName = "/Images/User/Weather/7.gif"; break; }
                    case 27: { FileName = "/Images/User/Weather/38.gif"; break; }
                    case 28: { FileName = "/Images/User/Weather/6.gif"; break; }
                    case 29: { FileName = "/Images/User/Weather/35.gif"; break; }
                    case 30: { FileName = "/Images/User/Weather/7.gif"; break; }
                    case 31: { FileName = "/Images/User/Weather/33.gif"; break; }
                    case 32: { FileName = "/Images/User/Weather/1.gif"; break; }
                    case 33: { FileName = "/Images/User/Weather/34.gif"; break; }
                    case 34: { FileName = "/Images/User/Weather/2.gif"; break; }
                    case 35: { FileName = "/Images/User/Weather/freezing_rain.png"; break; }
                    case 36: { FileName = "/Images/User/Weather/30.gif"; break; }
                    case 37: { FileName = "/Images/User/Weather/15.gif"; break; }
                    case 38: { FileName = "/Images/User/Weather/15.gif"; break; }
                    case 39: { FileName = "/Images/User/Weather/15.gif"; break; }
                    case 40: { FileName = "/Images/User/Weather/12.gif"; break; }
                    case 41: { FileName = "/Images/User/Weather/22.gif"; break; }
                    case 42: { FileName = "/Images/User/Weather/22.gif"; break; }
                    case 43: { FileName = "/Images/User/Weather/22.gif"; break; }
                    case 44: { FileName = "/Images/User/Weather/39.gif"; break; }
                    case 45: { FileName = "/Images/User/Weather/thundershowers.png"; break; }
                    case 46: { FileName = "/Images/User/Weather/19.gif"; break; }
                    case 47: { FileName = "/Images/User/Weather/thundershowers.png"; break; }
                    case 3200: { FileName = "/Images/User/Weather/1211810662.png"; break; }
                }
                }
                <img alt='@item.Text' title='@item.Text' src='@FileName'>
            </div>
            <div>
                <span>@item.High°</span>
                <span>@item.Low°</span>
            </div>
            @EndTD
            }
        }
        @EndTable
    </div>
</div>
من عکس‌های مربوط به وضعیت آب و هوا را در فایل ضمیمه قرار دادم.
چنانچه در مورد RSS وضعیت آب و هوای یاهو اطلاعات دقیق‌تری را می‌خواهید بدانید به این  لینک بروید.
در آموزش بعدی قصد دارم برایتان این بخش را توضیح دهم که بر اساس IP بازدید کننده سایت شما، اطلاعات آب و هوایی شهر بازدید کننده را برایش در سایت نمایش دهد.

Files-06bf65bac63d4dd694b15fc24d4cb074.zip

موفق باشید