یک گزینهی «هیچکدام» را هم اگر اضافه میکردید، بهتر میشد. چون WPF هدفش حذف این بستههای کامپوننت است و به شخصه تا به امروز فقط از یک سری template تغییر رنگ و شکل ظاهری کنترلهای استاندارد آن، از مورد دیگری استفاده نکردهام. یعنی واقعا نیازی نیست. تمام کنترلهای آن قابلیت سفارشی سازی کامل را دارند و همچنین اندکی هم جستجو کنید تمام آنها را با مثالهای فراوانی میتوانید پیدا کنید. این بستههای کامپوننت هم در کل چیزی نیستند به جز جمع آوری این مقالات و نظم دادن به آنها. یک نمونهی آن «Extended WPF Toolkit™ Community Edition » هست.
در این قسمت قصد داریم با نحوه پیاده سازی امتیاز دهی ستارهای به مطالب، که نمونهای از آنرا در سایت جاری در قسمتهای مختلف آن مشاهده میکنید، آشنا شویم.
مدل برنامه
در ابتدای کار نیاز است تا ساختاری را جهت ارائه لیستی از مطالب که دارای گزینه امتیاز دهی میباشند، تهیه کنیم:
اگر با EF Code first آشنا باشید، خاصیت Rating تعریف شده در اینجا میتواند از نوع ComplexType تعریف شود که شامل جمع امتیازهای داده شده، تعداد کل رای دهندهها و همچنین میانگین امتیازهای حاصل است.
منبع داده فرضی برنامه
در این مثال نیز از یک منبع داده فرضی تشکیل شده در حافظه استفاده خواهیم کرد تا امکان اجرای پروژه پیوستی را بدون نیاز به بانک اطلاعاتی خاصی و بدون نیاز به مقدمات برپایی آن، به سادگی داشته باشید.
در این منبع داده ابتدا لیستی از مطالب تهیه شده و سپس کش میشوند. در ادامه توسط متد GetLatestBlogPosts بازهای از این اطلاعات قابل بازیابی خواهند بود که برای استفاده در حالات صفحه بندی اطلاعات بهینه سازی شده است.
آشنایی با طراحی افزونه jQuery Star Rating
افزودن CSS نمایش امتیازها در ذیل هر مطلب
برای نمایش ستارهها و کار با تصویر Images/star_rating.png (که در پروژه پیوست قرار دارد) ابتدا نیاز است CSS فوق را به پروژه خود اضافه نمائید.
افزودن افزونه jQuery Star rating
اطلاعات فوق، فایل jquery.StarRating.js را تشکیل میدهند که باید به پروژه اضافه گردند.
کاری که این افزونه انجام میدهد ردیابی حرکت ماوس بر روی ستارههای نمایش داده شده و سپس ارسال سه پارامتر ذیل به اکشن متدی که توسط پارامتر postInfoUrl مشخص میگردد، پس از کلیک کاربر میباشد:
همانطور که ملاحظه میکنید به ازای هر قطعه رای گیری که به صفحه اضافه میشود، Id مطلب، رای داده شده و نام قسمت جاری، به اکشن متدی خاص ارسال خواهند گردید. sectionType از این جهت اضافه گردیده است تا بتوانید با بیش از یک جدول کار کنید و از این افزونه در قسمتهای مختلف سایت به سادگی بتوانید استفاده نمائید.
در اینجا از errorHandler برای نمایش خطاها، از completeHandler برای نمایش تشکر به کاربر و از onlyOneTimeHandler برای نمایش اخطار مثلا «یکبار بیشتر مجاز نیستید به ازای یک مطلب رای دهید»، میتوان استفاده کرد.
بنابراین تا اینجا فایل layout برنامه تقریبا چنین مداخلی را خواهد داشت:
طراحی یک HTML helper برای نمایش ستارههای امتیاز دهی
ابتدا پوشه استاندارد app_code را به پروژه اضافه کرده و سپس فایلی را به نام StarRatingHelper.cshtml، با محتوای ذیل به آن اضافه نمائید:
از این Html helper برای تشکیل ساختار نمایش قطعه امتیاز دهی به یک مطلب استفاده خواهیم کرد که توسط افزونه جیکوئری فوق ردیابی میشود.
کنترلر ذخیره سازی اطلاعات دریافتی برنامه
در اینجا کنترلری را که کار پردازش کلیک کاربر را بر روی امتیازی خاص انجام میدهد، ملاحظه میکنید.
امضای اکشن متد SaveRatings دقیقا بر اساس سه پارامتر ارسالی توسط jquery.StarRating.js که پیشتر توضیح داده شد، تعیین گردیده است. در این متد ابتدا بررسی میشود که آیا اطلاعاتی دریافت شده است یا خیر. اگر خیر، null را بازگشت خواهد داد. سپس توسط متد CanUserVoteBasedOnCookies بررسی میشود که آیا کاربر میتواند (خصوصا مجددا) رای دهد یا خیر. این افزونه برای رای دهی کاربران وارد نشده به سیستم نیز مناسب است. به همین جهت از کوکیها برای ثبت اطلاعات رای دادن کاربران استفاده گردیده است. پیاده سازی متد CanUserVoteBasedOnCookies را در ادامه ملاحظه خواهید نمود.
در ادامه در متد SaveRatings، یک switch تشکیل شده است تا بر اساس نام قسمت مرتبط به رای گیری، اطلاعات را بتوان به سرویس خاصی در برنامه هدایت کرد. مثلا اطلاعات قسمت مطالب به سرویس مطالب و قسمت نظرات به سرویس نظرات هدایت شوند.
متدهایی برای کار با کوکیها در ASP.NET MVC
در اینجا یک سری متد الحاقی را ملاحظه میکنید که برای ثبت اطلاعات رای داده شده یک کاربر بر اساس Id مطلب و نام قسمت متناظر با آن در یک کوکی طراحی شدهاند. بدیهی است اگر تمام قسمتهای برنامه شما محافظت شده هستند و کاربران حتما نیاز است ابتدا به سیستم لاگین نمایند، میتوانید این قسمت را حذف کرده و اطلاعات postId و SectionType را به ازای هر کاربر، جداگانه در بانک اطلاعاتی ثبت و بازیابی نمائید (دقیقترین حالت ممکن؛ البته برای سیستمی بسته که حتما تمام قسمتهای آن نیاز به اعتبار سنجی دارند).
پیشنهادی در مورد نحوه ذخیره سازی اطلاعات دریافتی
همانطور که عنوان شد، سه داده Id مطلب، رای داده شده و نام قسمت متناظر به اکشن متد ارسال میشود. از نام قسمت، برای انتخاب سرویس ذخیره سازی اطلاعات استفاده خواهیم کرد. این سرویس میتواند شامل متدی به نام SaveRating، همانند کدهای فوق باشد که Id مطلب و عدد رای حاصل به آن ارسال میگردند. ابتدا بر اساس این Id، مطلب متناظر یافت شده و سپس اطلاعات Rating آن به روز خواهد شد. در پایان هم ذخیره سازی اطلاعات باید صورت گیرد.
Viewهای برنامه
قسمت پایانی کار ما در اینجا تهیه دو View است:
الف) یک Partial view که لیست مطالب را به همراه گزینه رای دهی به آنها رندر میکند.
ب) View کاملی که از این Partial View استفاده کرده و همچنین افزونه jquery.StarRating.js را فراخوانی میکند.
کدهای _ItemsList.cshtml را در اینجا ملاحظه میکند که در آن نحوه فراخوانی متد کمکی StarRatingHelper.AddStarRating ذکر شده است.
اگر به کدهای آن دقت کنید از Regex.Replace برای حذف فاصلههای خالی و خطوط جدید بین تگها استفاده گردیده است. اگر اینکار انجام نشود، نیمههای ستارههای نمایش داده شده، با فاصله از یکدیگر رندر میشوند که صورت خوشایندی ندارد.
و نهایتا View ایی که از این اطلاعات استفاده میکنید ساختار زیر را خواهد داشت:
در این View لیستی از مطالب دریافت و به partial view طراحی شده برای نمایش ارسال میشود. سپس افزونه StarRating نیز تنظیم و به صفحه اضافه خواهد گردید. نکته مهم آن تعیین صحیح اکشن متدی است که قرار است اطلاعات را دریافت کند و نحوه مقدار دهی آنرا توسط متغیر postInfoUrl مشاهده میکنید.
دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample03.zip
مدل برنامه
در ابتدای کار نیاز است تا ساختاری را جهت ارائه لیستی از مطالب که دارای گزینه امتیاز دهی میباشند، تهیه کنیم:
namespace jQueryMvcSample03.Models { public class BlogPost { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } /// <summary> /// اطلاعات رای گیری یک مطلب به صورت یک خاصیت تو در تو یا پیچیده /// </summary> public Rating Rating { set; get; } public BlogPost() { Rating = new Rating(); } } }
namespace jQueryMvcSample03.Models { //[ComplexType] public class Rating { public double? TotalRating { get; set; } public int? TotalRaters { get; set; } public double? AverageRating { get; set; } } }
منبع داده فرضی برنامه
using System.Collections.Generic; using System.Linq; using jQueryMvcSample03.Models; namespace jQueryMvcSample03.DataSource { /// <summary> /// منبع داده فرضی /// </summary> public static class BlogPostDataSource { private static IList<BlogPost> _cachedItems; /// <summary> /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانیها صورت خواهد گرفت /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای /// تهیه یک لیست برای تمام کاربران سایت است /// </summary> static BlogPostDataSource() { _cachedItems = createBlogPostsInMemoryDataSource(); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> private static IList<BlogPost> createBlogPostsInMemoryDataSource() { var results = new List<BlogPost>(); for (int i = 1; i < 30; i++) { results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i, Rating = new Rating { TotalRaters = i + 1, AverageRating = 3.5 } }); } return results; } /// <summary> /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند /// شماره صفحه از یک شروع میشود /// </summary> public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4) { var skipRecords = pageNumber * recordsPerPage; return _cachedItems .OrderByDescending(x => x.Id) .Skip(skipRecords) .Take(recordsPerPage) .ToList(); } } }
در این منبع داده ابتدا لیستی از مطالب تهیه شده و سپس کش میشوند. در ادامه توسط متد GetLatestBlogPosts بازهای از این اطلاعات قابل بازیابی خواهند بود که برای استفاده در حالات صفحه بندی اطلاعات بهینه سازی شده است.
آشنایی با طراحی افزونه jQuery Star Rating
افزودن CSS نمایش امتیازها در ذیل هر مطلب
/* star rating system */ .post_rating { direction: ltr; } .rating { text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; display: inline-block; width: 8px; height: 16px; } .rating.stars { background-image: url('Images/star_rating.png'); } .rating.stars.active { cursor: pointer; } .star-left_off { background-position: -0px -0px; } .star-left_on { background-position: -16px -0px; } .star-right_off { background-position: -8px -0px; } .star-right_on { background-position: -24px -0px; }
افزودن افزونه jQuery Star rating
// <![CDATA[ (function ($) { $.fn.StarRating = function (options) { var defaults = { ratingStarsSpan: '.rating.stars', postInfoUrl: '/', loginUrl: '/login', errorHandler: null, completeHandler: null, onlyOneTimeHandler: null }; var options = $.extend(defaults, options); return this.each(function () { var ratingStars = $(this); $(ratingStars).unbind('mouseover'); $(ratingStars).mouseover(function () { var span = $(this).parent("span"); var newRating = $(this).attr("value"); setRating(span, newRating); }); $(ratingStars).unbind('mouseout'); $(ratingStars).mouseout(function () { var span = $(this).parent("span"); var rating = span.attr("rating"); setRating(span, rating); }); $(ratingStars).unbind('click'); $(ratingStars).click(function () { var span = $(this).parent("span"); var newRating = $(this).attr("value"); var text = span.children("span"); var pID = span.attr("post"); var type = span.attr("sectiontype"); postData({ postID: pID, rating: newRating, sectionType: type }); span.attr("rating", newRating); setRating(span, newRating); }); function setRating(span, rating) { span.find(options.ratingStarsSpan).each(function () { var value = parseFloat($(this).attr("value")); var imgSrc = $(this).attr("class"); if (value <= rating) $(this).attr("class", imgSrc.replace("_off", "_on")); else $(this).attr("class", imgSrc.replace("_on", "_off")); }); } function postData(dataJsonArray) { $.ajax({ type: "POST", url: options.postInfoUrl, data: JSON.stringify(dataJsonArray), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = options.loginUrl; } else if (status === 'error' || !data) { if (options.errorHandler) options.errorHandler(this); } else if (data == "nok") { if (options.onlyOneTimeHandler) options.onlyOneTimeHandler(this); } else { if (options.completeHandler) options.completeHandler(this); } } }); } }); }; })(jQuery); // ]]>
کاری که این افزونه انجام میدهد ردیابی حرکت ماوس بر روی ستارههای نمایش داده شده و سپس ارسال سه پارامتر ذیل به اکشن متدی که توسط پارامتر postInfoUrl مشخص میگردد، پس از کلیک کاربر میباشد:
{ postID: pID, rating: newRating, sectionType: type }
در اینجا از errorHandler برای نمایش خطاها، از completeHandler برای نمایش تشکر به کاربر و از onlyOneTimeHandler برای نمایش اخطار مثلا «یکبار بیشتر مجاز نیستید به ازای یک مطلب رای دهید»، میتوان استفاده کرد.
بنابراین تا اینجا فایل layout برنامه تقریبا چنین مداخلی را خواهد داشت:
<head> <title>@ViewBag.Title</title> <link href="@Url.Content("Content/starRating.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.StarRating.js")" type="text/javascript"></script> @RenderSection("JavaScript", required: false) </head>
طراحی یک HTML helper برای نمایش ستارههای امتیاز دهی
ابتدا پوشه استاندارد app_code را به پروژه اضافه کرده و سپس فایلی را به نام StarRatingHelper.cshtml، با محتوای ذیل به آن اضافه نمائید:
@using System.Globalization @helper AddStarRating(int postId, double? average = 0, int? postRatingsCount = 0, string type = "BlogPost", string tooltip = "لطفا جهت رای دادن کلیک نمائید") { string actIt = "active "; if (!average.HasValue) { average = 0; } if (!postRatingsCount.HasValue) { postRatingsCount = 0; } <span class='postRating' rating='@average' post='@postId' title='@tooltip' sectiontype='@type'> @for (double i = .5; i <= 5.0; i = i + .5) { string left; if (i <= average) { left = (i * 2) % 2 == 1 ? "left_on" : "right_on"; } else { left = (i * 2) % 2 == 1 ? "left_off" : "right_off"; } <span class='rating stars @(actIt)star-@left' value='@i'></span> } @if (postRatingsCount > 0) { var ratingInfo = string.Format(CultureInfo.InvariantCulture, "امتیاز {0:0.00} از 5 توسط {1} نفر", average, postRatingsCount); <span>@ratingInfo</span> } else { <span></span> } </span> }
کنترلر ذخیره سازی اطلاعات دریافتی برنامه
using System.Web.Mvc; using System.Web.UI; using jQueryMvcSample03.DataSource; using jQueryMvcSample03.Security; namespace jQueryMvcSample03.Controllers { public class HomeController : Controller { public ActionResult Index() { var postsList = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0); return View(postsList); //نمایش صفحه اصلی } [HttpPost] [AjaxOnly] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public ActionResult SaveRatings(int? postId, double? rating, string sectionType) { if (postId == null || rating == null || string.IsNullOrWhiteSpace(sectionType)) return Content(null); //اعلام بروز خطا if (!this.HttpContext.CanUserVoteBasedOnCookies(postId.Value, sectionType)) return Content("nok"); //اعلام فقط یکبار مجاز هستید رای دهید switch (sectionType) //قسمتهای مختلف سایت که در جداول مختلفی قرار دارند نیز میتوانند گزینه امتیاز دهی داشته باشند { case "BlogPost": //الان شماره مطلب و رای ارسالی را داریم که میتوان نسبت به ذخیره آن اقدام کرد //مثلا //_blogPostsService.SaveRating(postId.Value, rating.Value); break; //... سایر قسمتهای دیگر سایت default: return Content(null); //اعلام بروز خطا } return Content("ok"); //اعلام موفقیت آمیز بودن ثبت اطلاعات } [HttpGet] public ActionResult Post(int? id) { if (id == null) return Redirect("/"); //todo: show the content here return Content("Post " + id.Value); } } }
امضای اکشن متد SaveRatings دقیقا بر اساس سه پارامتر ارسالی توسط jquery.StarRating.js که پیشتر توضیح داده شد، تعیین گردیده است. در این متد ابتدا بررسی میشود که آیا اطلاعاتی دریافت شده است یا خیر. اگر خیر، null را بازگشت خواهد داد. سپس توسط متد CanUserVoteBasedOnCookies بررسی میشود که آیا کاربر میتواند (خصوصا مجددا) رای دهد یا خیر. این افزونه برای رای دهی کاربران وارد نشده به سیستم نیز مناسب است. به همین جهت از کوکیها برای ثبت اطلاعات رای دادن کاربران استفاده گردیده است. پیاده سازی متد CanUserVoteBasedOnCookies را در ادامه ملاحظه خواهید نمود.
در ادامه در متد SaveRatings، یک switch تشکیل شده است تا بر اساس نام قسمت مرتبط به رای گیری، اطلاعات را بتوان به سرویس خاصی در برنامه هدایت کرد. مثلا اطلاعات قسمت مطالب به سرویس مطالب و قسمت نظرات به سرویس نظرات هدایت شوند.
متدهایی برای کار با کوکیها در ASP.NET MVC
using System; using System.Web; namespace jQueryMvcSample03.Security { public static class CookieHelper { public static bool CanUserVoteBasedOnCookies(this HttpContextBase httpContext, int postId, string sectionType) { string key = sectionType + "-" + postId; var value = httpContext.GetCookieValue(key); if (string.IsNullOrWhiteSpace(value)) { httpContext.AddCookie(key, key); return true; } return false; } public static void AddCookie(this HttpContextBase httpContextBase, string cookieName, string value) { httpContextBase.AddCookie(cookieName, value, DateTime.Now.AddDays(30)); } public static void AddCookie(this HttpContextBase httpContextBase, string cookieName, string value, DateTime expires) { var cookie = new HttpCookie(cookieName) { Expires = expires, Value = httpContextBase.Server.UrlEncode(value) // For Cookies and Unicode characters }; httpContextBase.Response.Cookies.Add(cookie); } public static string GetCookieValue(this HttpContextBase httpContext, string cookieName) { var cookie = httpContext.Request.Cookies[cookieName]; if (cookie == null) return string.Empty; //cookie doesn't exist // For Cookies and Unicode characters return httpContext.Server.UrlDecode(cookie.Value); } } }
پیشنهادی در مورد نحوه ذخیره سازی اطلاعات دریافتی
using jQueryMvcSample03.Models; namespace jQueryMvcSample03.DataSource { public interface IBlogPostsService { void SaveRating(int postId, double rating); } public class SampleService : IBlogPostsService { /// <summary> /// یک نمونه از متد ذخیره سازی اطلاعات پیشنهادی /// فقط برای ایده گرفتن /// بدیهی است محل قرارگیری اصلی آن در لایه سرویس برنامه شما خواهد بود /// </summary> public void SaveRating(int postId, double rating) { BlogPost post = null; //post = _blogCtx.Find(postId); // بر اساس شماره مطلب، مطلب یافت شده و فیلدهای آن تنظیم میشوند if (post == null) return; if (!post.Rating.TotalRaters.HasValue) post.Rating.TotalRaters = 0; if (!post.Rating.TotalRating.HasValue) post.Rating.TotalRating = 0; if (!post.Rating.AverageRating.HasValue) post.Rating.AverageRating = 0; post.Rating.TotalRaters++; post.Rating.TotalRating += rating; post.Rating.AverageRating = post.Rating.TotalRating / post.Rating.TotalRaters; // todo: call save changes at the end. } } }
Viewهای برنامه
قسمت پایانی کار ما در اینجا تهیه دو View است:
الف) یک Partial view که لیست مطالب را به همراه گزینه رای دهی به آنها رندر میکند.
ب) View کاملی که از این Partial View استفاده کرده و همچنین افزونه jquery.StarRating.js را فراخوانی میکند.
@using System.Text.RegularExpressions @model IList<jQueryMvcSample03.Models.BlogPost> <ul> @foreach (var item in Model) { <li> <fieldset> <legend>مطلب @item.Id</legend> <h5> @Html.ActionLink(linkText: item.Title, actionName: "Post", controllerName: "Home", routeValues: new { id = item.Id }, htmlAttributes: null) </h5> @item.Body <div class="post_rating"> @Html.Raw(Regex.Replace(@StarRatingHelper.AddStarRating(item.Id, item.Rating.AverageRating, item.Rating.TotalRaters, "BlogPost").ToHtmlString(), @">\s+<", "><")) </div> </fieldset> </li> } </ul>
اگر به کدهای آن دقت کنید از Regex.Replace برای حذف فاصلههای خالی و خطوط جدید بین تگها استفاده گردیده است. اگر اینکار انجام نشود، نیمههای ستارههای نمایش داده شده، با فاصله از یکدیگر رندر میشوند که صورت خوشایندی ندارد.
و نهایتا View ایی که از این اطلاعات استفاده میکنید ساختار زیر را خواهد داشت:
@model IList<jQueryMvcSample03.Models.BlogPost> @{ ViewBag.Title = "Index"; var postInfoUrl = Url.Action(actionName: "SaveRatings", controllerName: "Home"); } <h2> سیستم امتیاز دهی</h2> @{ Html.RenderPartial("_ItemsList", Model); } @section JavaScript { <script type="text/javascript"> $(document).ready(function () { $(".rating.stars.active").StarRating({ ratingStarsSpan: '.rating.stars', postInfoUrl: '@postInfoUrl', loginUrl: '/login', errorHandler: function () { alert('خطایی رخ داده است'); }, completeHandler: function () { alert('با تشکر! رای شما با موفقیت ثبت شد'); }, onlyOneTimeHandler: function () { alert('فقط یکبار میتوانید به ازای هر مطلب رای دهید'); } }); }); </script> }
دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample03.zip
PDFKit
A JavaScript PDF generation library for Node and the browser
نمونه مثال ایجاد شده
روشهای مختلف دریافت و نصب بوت استرپ 4
اولین کاری را که باید جهت شروع به کار با بوت استرپ 4 انجام داد، نصب و افزودن آن به صفحهی HTML جاری است. روشهای زیادی برای انجام اینکار وجود دارند:
الف) استفاده از نگارش SASS آن
بوت استرپ 4 در اصل مبتنی بر SASS توسعه یافتهاست و فایلهای آن با فرمت scss. ارائه میشوند. مزیت کار با این روش، امکان سفارشی سازی بوت استرپ 4 و یا مشارکت در پروژهی آن است و بدیهی است پس از آن باید SASS را به CSS کامپایل و مورد استفاده قرار داد.
ب) استفاده از CDN و یا Content delivery network
- مزیت آن بالا رفتن سرعت سایت با کش شدن آن در شبکه و یا شبکههای توزیع محتوا است.
- اما این روش محدودیت و الزام کار آنلاین با فایلهای بوت استرپ را نیز به همراه دارد.
برای کار با CDNهای بوت استرپ، مطابق راهنمای آن، تنها کافی است مدخل فایل css آنرا به head صفحه و مداخل فایلهای js ذیل را پیش از بسته شدن تگ body قرار دهید:
در اینجا شاید نام دو فایل، برای شما تازگی داشته باشند:
- jquery-3.3.1.slim : در اینجا slim یک نگارش بسیار کوچک از jQuery میباشد که بوت استرپ 4 بر مبنای آن کار میکند. البته در یک پروژهی واقعی احتمالا نیاز به نگارش کامل آنرا خواهید داشت و یا اگر قصد حذف کردن جیکوئری را دارید، این نگارش، کمحجمترین آن است.
- popper.min.js : برای نمونه Bootstrap dropdown برای کارکرد صحیح آن در نگارش 4، نیاز به این وابستگی جدید را دارد.
ج) استفاده از فایلهای از پیش پردازش شده
فایلهای از پیش آماده شدهی آنرا میتوان مستقیما از سایت بوت استرپ، با کلیک بر روی دکمهی download واقع در منوی راهبری سایت آن، دریافت کرد. مزیت این روش، امکان کار و توسعهی آفلاین صفحات مبتنی بر بوت استرپ است.
مشکل این روش عدم اطلاع رسانی خودکار از ارائهی نگارشهای جدید و نیاز به دریافت دستی مجدد این بسته، به ازای هر نگارش جدید آن میباشد.
د) استفاده از ابزارهای مدیریت بستهها
روشی را که ما در اینجا از آن استفاده خواهیم کرد، دریافت و نصب وابستگیهای مورد نیاز جهت کار با بوت استرپ 4، توسط npm است. به همین جهت یک فایل جدید package.json را با محتوای ذیل ایجاد کنید:
سپس از طریق خط فرمان به این پوشه وارد شده و دستور npm install را جهت دریافت این وابستگیها صادر کنید. یکی از مزیتهای مهم این روش، آگاه شدن خودکار از به روز رسانیهای وابستگیهای فوق، توسط افزونههایی مانند Version Lens است.
در اینجا font-awesome را نیز مشاهده میکنید؛ از این جهت که بوت استرپ 4 برخلاف نگارش 3 آن، به همراه گلیف آیکنهای پیشفرض آن نیست.
ایجاد قالب ابتدایی شروع به کار با بوت استرپ 4
پس از دریافت وابستگیهای مورد نیاز جهت شروع به کار با بوت استرپ 4 که هم اکنون باید در پوشهی node_modules واقع در ریشهی پوشهی جاری موجود باشند، در ادامه حداقل قالبی را که برای کار با آن نیاز است، مرور میکنیم:
- در ابتدا سازگاری با edge و الزام به استفادهی از آخرین نگارش IE نصب شده مشخص شدهاست.
- سپس viewport استاندارد، جهت تعیین اینکه این صفحه با ابزارهای موبایل نیز سازگار است، تعریف شدهاست.
- در قسمت head، مدخل فایل bootstrap.min.css تعریف شدهاست. همچنین مداخل مورد نیاز جهت کار با font-awesome را نیز مشاهده میکنید.
- پیش از بسته شدن تگ body، تعاریف jQuery، کتابخانهی popper و سپس bootstrap.min.js قید شدهاند. کتابخانهی popper از مسیر umd آن دریافت شدهاست تا همه جا کار کند.
نکتهی مهم!
در نگارش نهایی برنامهی شما، مسیرهای فایلهای شروع شدهی با /node_modules/ نباید وجود داشته باشند. این فایلها را بهتر است توسط ابزارهای bundling & minification یکی و سپس به صفحه اضافه کنید.
غنی سازی ویرایشگر VSCode برای کار سادهتر با بوت استرپ
VSCode یک ویرایشگر حرفهای چندسکویی است که برای ویندوز، مک و لینوکس تهیه شدهاست. این ویرایشگر را میتوان توسط افزونههای زیر برای کار سادهتر با بوت استرپ غنی کرد:
Bootstrap 4, Font awesome 4, Font Awesome 5 Free & Pro snippets: ساده سازی تشکیل تگهای بوت استرپ
Path Autocomplete: کار وارد کردن مسیر فایلها و تصاویر را ساده میکند.
HTML CSS Support: کار آن غنی سازی intellisense این ویرایشگر جهت کار با ویژگیها و همچنین کلاسهای CSS است.
IntelliSense for CSS class names in HTML: انتخاب کلاسهای CSS بوت استرپ را سادهتر میکند.
Live Server: کار آن راه اندازی یک وب سرور آزمایشی و سپس امکان مشاهدهی آنی تغییرات در برنامه و فایل HTML جاری، در مرورگر میباشد.
برای کار با آن، در حالیکه صفحهی HTML جاری در VSCode باز است، بر روی دکمهی Go Live اضافه شدهی در status bar آن کلیک کنید. پس از آن، یک وب سرور آزمایشی را بر روی پورت 5500 آغاز کرده و صفحهی جاری را در آدرس http://127.0.0.1:5500/index.html در مرورگر پیشفرض سیستم نمایش میدهد. اکنون فایل HTML خود را در VSCode ویرایش کنید. ملاحظه خواهید کرد که بلافاصله این تغییرات در مرورگر قابل مشاهده هستند.
نگارشهای راست به چپ بوت استرپ 4
قرار است بوت استرپ 4 نگارش رسمی راست به چپ نیز داشته باشد. به همین منظور میتوانید در اینجا رای خود را اظهار کنید.
همچنین پروژههای زیر نیز چنین قابلیتی را ارائه میدهند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: Bootstrap4_01.zip
اولین کاری را که باید جهت شروع به کار با بوت استرپ 4 انجام داد، نصب و افزودن آن به صفحهی HTML جاری است. روشهای زیادی برای انجام اینکار وجود دارند:
الف) استفاده از نگارش SASS آن
بوت استرپ 4 در اصل مبتنی بر SASS توسعه یافتهاست و فایلهای آن با فرمت scss. ارائه میشوند. مزیت کار با این روش، امکان سفارشی سازی بوت استرپ 4 و یا مشارکت در پروژهی آن است و بدیهی است پس از آن باید SASS را به CSS کامپایل و مورد استفاده قرار داد.
ب) استفاده از CDN و یا Content delivery network
- مزیت آن بالا رفتن سرعت سایت با کش شدن آن در شبکه و یا شبکههای توزیع محتوا است.
- اما این روش محدودیت و الزام کار آنلاین با فایلهای بوت استرپ را نیز به همراه دارد.
برای کار با CDNهای بوت استرپ، مطابق راهنمای آن، تنها کافی است مدخل فایل css آنرا به head صفحه و مداخل فایلهای js ذیل را پیش از بسته شدن تگ body قرار دهید:
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
- jquery-3.3.1.slim : در اینجا slim یک نگارش بسیار کوچک از jQuery میباشد که بوت استرپ 4 بر مبنای آن کار میکند. البته در یک پروژهی واقعی احتمالا نیاز به نگارش کامل آنرا خواهید داشت و یا اگر قصد حذف کردن جیکوئری را دارید، این نگارش، کمحجمترین آن است.
- popper.min.js : برای نمونه Bootstrap dropdown برای کارکرد صحیح آن در نگارش 4، نیاز به این وابستگی جدید را دارد.
ج) استفاده از فایلهای از پیش پردازش شده
فایلهای از پیش آماده شدهی آنرا میتوان مستقیما از سایت بوت استرپ، با کلیک بر روی دکمهی download واقع در منوی راهبری سایت آن، دریافت کرد. مزیت این روش، امکان کار و توسعهی آفلاین صفحات مبتنی بر بوت استرپ است.
مشکل این روش عدم اطلاع رسانی خودکار از ارائهی نگارشهای جدید و نیاز به دریافت دستی مجدد این بسته، به ازای هر نگارش جدید آن میباشد.
د) استفاده از ابزارهای مدیریت بستهها
روشی را که ما در اینجا از آن استفاده خواهیم کرد، دریافت و نصب وابستگیهای مورد نیاز جهت کار با بوت استرپ 4، توسط npm است. به همین جهت یک فایل جدید package.json را با محتوای ذیل ایجاد کنید:
{ "name": "bootstrap.4", "version": "1.0.0", "description": "client side resources of the project", "scripts": {}, "author": "VahidN", "license": "ISC", "dependencies": { "bootstrap": "^4.1.3", "components-font-awesome": "5.0.6", "jquery": "^3.3.1", "popper.js": "^1.14.4" } }
در اینجا font-awesome را نیز مشاهده میکنید؛ از این جهت که بوت استرپ 4 برخلاف نگارش 3 آن، به همراه گلیف آیکنهای پیشفرض آن نیست.
ایجاد قالب ابتدایی شروع به کار با بوت استرپ 4
پس از دریافت وابستگیهای مورد نیاز جهت شروع به کار با بوت استرپ 4 که هم اکنون باید در پوشهی node_modules واقع در ریشهی پوشهی جاری موجود باشند، در ادامه حداقل قالبی را که برای کار با آن نیاز است، مرور میکنیم:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="/node_modules/components-font-awesome/css/fa-solid.min.css"> <link rel="stylesheet" href="/node_modules/components-font-awesome/css/fontawesome.min.css"> <title>Bootstrap</title> </head> <body> <div class="container"> </div> <script src="/node_modules/jquery/dist/jquery.min.js"></script> <script src="/node_modules/popper.js/dist/umd/popper.min.js"></script> <script src="/node_modules/bootstrap/dist/js/bootstrap.min.js"></script> </body> </html>
- سپس viewport استاندارد، جهت تعیین اینکه این صفحه با ابزارهای موبایل نیز سازگار است، تعریف شدهاست.
- در قسمت head، مدخل فایل bootstrap.min.css تعریف شدهاست. همچنین مداخل مورد نیاز جهت کار با font-awesome را نیز مشاهده میکنید.
- پیش از بسته شدن تگ body، تعاریف jQuery، کتابخانهی popper و سپس bootstrap.min.js قید شدهاند. کتابخانهی popper از مسیر umd آن دریافت شدهاست تا همه جا کار کند.
نکتهی مهم!
در نگارش نهایی برنامهی شما، مسیرهای فایلهای شروع شدهی با /node_modules/ نباید وجود داشته باشند. این فایلها را بهتر است توسط ابزارهای bundling & minification یکی و سپس به صفحه اضافه کنید.
غنی سازی ویرایشگر VSCode برای کار سادهتر با بوت استرپ
VSCode یک ویرایشگر حرفهای چندسکویی است که برای ویندوز، مک و لینوکس تهیه شدهاست. این ویرایشگر را میتوان توسط افزونههای زیر برای کار سادهتر با بوت استرپ غنی کرد:
Bootstrap 4, Font awesome 4, Font Awesome 5 Free & Pro snippets: ساده سازی تشکیل تگهای بوت استرپ
Path Autocomplete: کار وارد کردن مسیر فایلها و تصاویر را ساده میکند.
HTML CSS Support: کار آن غنی سازی intellisense این ویرایشگر جهت کار با ویژگیها و همچنین کلاسهای CSS است.
IntelliSense for CSS class names in HTML: انتخاب کلاسهای CSS بوت استرپ را سادهتر میکند.
Live Server: کار آن راه اندازی یک وب سرور آزمایشی و سپس امکان مشاهدهی آنی تغییرات در برنامه و فایل HTML جاری، در مرورگر میباشد.
برای کار با آن، در حالیکه صفحهی HTML جاری در VSCode باز است، بر روی دکمهی Go Live اضافه شدهی در status bar آن کلیک کنید. پس از آن، یک وب سرور آزمایشی را بر روی پورت 5500 آغاز کرده و صفحهی جاری را در آدرس http://127.0.0.1:5500/index.html در مرورگر پیشفرض سیستم نمایش میدهد. اکنون فایل HTML خود را در VSCode ویرایش کنید. ملاحظه خواهید کرد که بلافاصله این تغییرات در مرورگر قابل مشاهده هستند.
نگارشهای راست به چپ بوت استرپ 4
قرار است بوت استرپ 4 نگارش رسمی راست به چپ نیز داشته باشد. به همین منظور میتوانید در اینجا رای خود را اظهار کنید.
همچنین پروژههای زیر نیز چنین قابلیتی را ارائه میدهند:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: Bootstrap4_01.zip
اشتراکها
کتابخانه Gijgo
jQuery Grid by Gijgo.com is a plug-in for the jQuery Javascript library.
It is a very fast and extandable tool, and will add advanced interaction controls to any HTML table.
This plugin support pagination, javascript and server side data sources, jQuery UI and Bootstrap. Demo
نظرات اشتراکها
دریافت کتاب Pro ASP.NET MVC 5 Platform
با عرض سلام و احترام
منظور از ASP.NET در اینجا دقیقا چی است ؟ و ارتباط مستقیم آن با ASP.NET MVC چیست ؟
به ASP.NET Web Forms که کاری نداریم، ولی اگر منظورتان از ASP.NET همان ASP.NET Handlers & Modules است و امکاناتی نظیر Global.asax و ...، می توان یک سایت ASP.NET MVC را به صورت کامل و بدون این امکانات بارگذاری کرد، حتی بدون استفاده از System.Web.dll ، سالهاست ASP.NET MVC دیگر وابستگی به ASP.NET ندارد، همانطور که ASP.NET Web API هیچ ربطی به ASP.NET MVC ندارد.
- تمرین تغییر مداوم و تست واحد | (Afshar Mohebbi) | blog.afsharm.com
- چندین نکته هنگام استفاده از تابع AVG در SQL SERVER | محمد صاحب | www.dotnetdev.info
- کاهش مشکلات خروج افراد از تیم | (Afshar Mohebbi) | blog.afsharm.com
- کروم در آستانه پیش افتادن از فایرفاکس | علی پارسا | www.winbeta.net
- Silverlight Toolkit September 2011 منتشر شد | silverlight.codeplex.com
- جهت گیری اصلی نگارش بعدی ویندوز سرور، گزینهی Server Core و برنامههای بدون رابط کاربری هستند | www.infoq.com
- ماکروها از Visual Studio 11 حذف شدهاند | www.infoq.com
- مایکروسافت Start Menu رو حذف کرده چون کسی ازش استفاده نمیکنه! | rss.slashdot.org
- مروری بر امکانات نگارش جدید Silverlight Toolkit | blogs.msdn.com
اشتراکها
بهبودهای WPF در NET 4.6.1.
With the 4.6.1 RC we have added support for WPF to recognize custom dictionaries registered globally. This capability is available in addition to the ability to register them per-control. Also, custom dictionaries in the previous versions of WPF had no affordance for Excluded Words and AutoCorrect lists. On Windows 8.1 and Windows 10, these scenarios are now enabled through the use of files that can be placed under %AppData%\Microsoft\Spelling\<language tag>.
در قسمت قبل مقدمه ای راجع به انواع منابع موجود در ASP.NET و برخی مسائل پیرامون آن ارائه شد. در این قسمت راجع به نحوه رفتار ASP.NET در برخورد با انواع منابع بحث میشود.
مدیریت منابع در ASP.NET
در مدل پرووایدر منابع در ASP.NET کار مدیریت منابع از کلاس ResourceProviderFactory شروع میشود. این کلاس که از نوع abstract تعریف شده است، دو متد برای فراهم کردن پرووایدرهای کلی و محلی دارد.
کلاس پیشفرض در ASP.NET برای پیادهسازی ResourceProviderFactory در اسمبلی System.Web قرار دارد. این کلاس که ResXResourceProviderFactory نام دارد نمونههایی از کلاسهای LocalResxResourceProvider و GlobalResxResourceProvider را برمیگرداند. درباره این کلاسها در ادامه بیشتر بحث خواهد شد.
نکته: هر سه کلاس پیشفرض اشاره شده در بالا و نیز سایر کلاسهای مربوط به عملیات مدیریت منابع در آنها، همگی در فضای نام System.Web.Compilation قرار دارند و متاسفانه دارای سطح دسترسی internal هستند. بنابراین به صورت مستقیم در دسترس نیستند.
برای نمونه با توجه به تصویر فرضی نشان داده شده در قسمت قبل، در اولین بارگذاری صفحه SubDir1\Page1.aspx عبارات ضمنی بکاربرده شده در این صفحه برای منابع محلی (در قسمت قبل شرح داده شده است) باعث فراخوانی متد مربوط به Local Resources در کلاس ResXResourceProviderFactory میشود. این متد نمونهای از کلاس LocalResXResourceProvider برمیگرداند. (در ادامه با نحوه سفارشیسازی این کلاسها نیز آشنا خواهیم شد).
رفتار پیشفرض این پرووایدر این است که نمونهای از کلاس ResourceManager با توجه به کلید درخواستی برای صفحه موردنظر (مثلا نوع Page1.aspx در اسمبلی App_LocalResources.subdir1.XXXXXX که در تصویر موجود در قسمت قبل نشان داده شده است) تولید میکند. حال این کلاس با استفاده از کالچر مربوط به درخواست موردنظر، ورودی موردنظر را از منبع مربوطه استخراج میکند. مثلا اگر کالچر موردبحث es (اسپانیایی) باشد، اسمبلی ستلایت موجود در مسیر نسبی \es\ انتخاب میشود.
برای روشنتر شدن بحث به تصویر زیر که عملیات مدیریت منابع پیش فرض در ASP.NET در درخواست صفحه Page1.aspx از پوشه SubDir1 را نشان میدهد، دقت کنید:
همانطور که در قسمت اول این سری مطالب عنوان شد، رفتار کلاس ResourceManager برای یافتن کلیدهای Resource، استخراج آن از نزدیکترین گزینه موجود است. یعنی مثلا برای یافتن کلیدی در کالچر es در مثال بالا، ابتدا اسمبلیهای مربوط به این کالچر جستجو میشود و اگر ورودی موردنظر یافته نشد، جستجو در اسمبلیهای ستلایت پیشفرض سیستم موجود در ریشه فولدر bin برنامه ادامه مییابد، تا درنهایت نزدیکترین گزینه پیدا شود (فرایند fallback).
نکته: همانطور که در تصویر بالا نیز مشخص است، نحوه نامگذاری اسمبلی منابع محلی به صورت <App_LocalResources.<SubDirectory>.<A random code است.
نکته: پس از اولین بارگذاری هر اسمبلی، آن اسمبلی به همراه خود نمونه کلاس ResourceManager که مثلا توسط کلاس LocalResXResourceProvider تولید شده است در حافظه سرور کش میشوند تا در استفادههای بعدی به کار روند.
نکته: فرایند مشابهای برای یافتن کلیدها در منابع کلی (Global Resources) به انجام میرسد. تنها تفاوت آن این است که کلاس ResXResourceProviderFactory نمونهای از کلاس GlobalResXResourceProvider تولید میکند.
چرا پرووایدر سفارشی؟
تا اینجا بالا با کلیات عملیاتی که ASP.NET برای بارگذاری منابع محلی و کلی به انجام میرساند، آشنا شدیم. حالا باید به این پرسش پاسخ داد که چرا پرووایدری سفارشی نیاز است؟ علاوه بر دلایلی که در قسمتهای قبلی به آنها اشاره شد، میتوان دلایل زیر را نیز برشمرد:
- استفاده از منابع و یا اسمبلیهای ستلایت موجود - اگر بخواهید در برنامه خود از اسمبلیهایی مشترک، بین برنامههای ویندوزی و وبی استفاده کنید، و یا بخواهید به هردلیلی از اسمبلیهای جداگانهای برای این منابع استفاده کنید، مدل پیشفرض موجود در ASP.NET جوابگو نخواهد بود.
- استفاده از منابع دیگری به غیر از فایلهای resx. مثل دیتابیس - برای برنامههای تحت وب که صفحات بسیار زیاد به همراه ورودیهای بیشماری از Resourceها دارند، استفاده از مدل پرووایدر منابع پیشفرض در ASP.NET و ذخیره تمامی این ورودیها درون فایلهای resx. بار نسبتا زیادی روی حافظه سرور خواهد گذاشت. درصورت مدیریت بهینه فراخوانیهای سمت دیتابیس میتوان با بهرهبرداری از جداول یک دیتابیس به عنوان منبع، کمک زیادی به وب سرور کرد! همچنین با استفاده از دیتابیس میتوان مدیریت بهتری بر ورودیها داشت و نیز امکان ذخیرهسازی حجم بیشتری از دادهها در اختیار توسعه دهنده قرار خواهد گرفت.
البته به غیر از دیتابیس و فایلهای resx. نیز گزینههای دیگری برای ذخیرهسازی ورودیهای این منابع وجود دارند. به عنوان مثال میتوان مدیریت این منابع را کلا به سیستم دیگری سپرد و درخواست ورودیهای موردنیاز را به یکسری وبسرویس سپرد. برای پیاده سازی چنین سیستمی نیاز است تا مدلی سفارشی تهیه و استفاده شود.
- پیاده سازی امکان به روزرسانی منابع در زمان اجرا - درصورتیکه بخواهیم امکان بروزرسانی ورودیها را در زمان اجرا در استفاده از فایلهای resx. داشته باشیم، یکی از راهحلها، سفارشی سازی این پرووایدرهاست.
مدل پرووایدر منابع
همانطور که قبلا هم اشاره شد، وظیفه استخراج دادهها از Resourceها به صورت پیشفرض، درنهایت بر عهده نمونهای از کلاس ResourceManager است. در واقع این کلاس کل فرایند انتخاب مناسبترین کلید از منابع موجود را با توجه به کالچر رابط کاربری (UI Culture) در ثرد جاری کپسوله میکند. درباره این کلاس در ادامه بیشتر بحث خواهد شد.
همچنین بازهم همانطور که قبلا توضیح داده شد، استفاده از ورودیهای منابع موجود به دو روش انجام میشود. استفاده از عبارات بومیسازی و نیز با استفاده از برنامهنویسی که ازطریق دومتد GetLocalResourceObject و GetGlobalResourceObject انجام میشود. درضمن کلیه عبارات بومیسازی در زمان رندر صفحات وب درنهایت تبدیل به فراخوانیهایی از این دو متد در کلاس TemplateControl خواهند شد.
عملیات پس از فراخوانی این دو متد جایی است که مدل Resource Provider پیشفرض ASP.NET وارد کار میشود. این فرایند ابتدا با فراخوانی نمونهای از کلاس ResourceProviderFactory آغاز میشود که پیادهسازی پیشفرض آن در کلاس ResXResourceProviderFactory قرار دارد.
این کلاس سپس با توجه به نوع منبع درخواستی (Global یا Local) نمونهای از پرووایدر مربوطه (که باید اینترفیس IResourceProvider را پیادهسازی کرده باشند) را تولید میکند. پیادهسازی پیشفرض این پرووایدرها در ASP.NET در کلاسهای GlobalResXResourceProvider و LocalResXResourceProvider قرار دارد.
این پروایدرها درنهایت باتوجه به محل ورودی درخواستی، نمونه مناسب از کلاس RsourceManager را تولید و استفاده میکنند.
همچنین در پروایدرهای محلی، برای استفاده از عبارات بومیسازی ضمنی، نمونهای از کلاس ResourceReader مورد استفاده قرار میگیرد. در زمان تجزیه و تحلیل صفحه وب درخواستی در سرور، با استفاده از این کلاس کلیدهای موردنظر یافته میشوند. این کلاس درواقع پیادهسازی اینترفیس IResourceReader بوده که حاوی یک Enumerator که جفت دادههای Key-Value از کلیدهای Resource را برمیگرداند، است.
تصویر زیر نمایی کلی از فرایند پیشفرض موردبحث را نشان میدهد:
این فرایند باتوجه به پیاده سازی نسبتا جامع آن، قابلیت بسیاری برای توسعه و سفارشی سازی دارد. بنابراین قبل از ادامه مبحث بهتر است، کلاسهای اصلی این مدل بیشتر شرح داده شوند.
پیادهسازیها
کلاس ResourceProviderFactory به صورت زیر تعریف شده است:
public abstract class ResourceProviderFactory { public abstract IResourceProvider CreateGlobalResourceProvider(string classKey); public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath); }
پس از تولید پرووایدر موردنظر با استفاده از متد مناسب با توجه به شرایط شرح داده شده در بالا، نمونه تولیدشده از کلاس پرووایدر موردنظر وظیفه فراهمکردن کلیدهای Resource را برعهده دارد. پرووایدرهای موردبحث باید اینترفیس IResourceProvider را که به صورت زیر تعریف شده است، پیاده سازی کنند:
public interface IResourceProvider { IResourceReader ResourceReader { get; } object GetObject(string resourceKey, CultureInfo culture); }
همانطور که میبینید این پرووایدرها باید یک RsourceReader برای خواندن کلیدهای Resource فراهم کنند. همچنین یک متد با عنوان GetObject که کار اصلی برگرداندن داده ذخیرهشده در ورودی موردنظر را برعهده دارد باید در این پرووایدرها پیادهسازی شود. همانطور که قبلا اشاره شد، پیادهسازی پیشفرض این کلاسها درنهایت نمونهای از کلاس ResourceManager را برای یافتن مناسبترین گزینه از بین کلیدهای موجود تولید میکند. این نمونه مورد بحث در متد GetObject مورد استفاده قرار میگیرد.
نکته: کدهای نشاندادهشده در ادامه مطلب با استفاده از ابزار محبوب ReSharper استخراج شدهاند. این ابزار برای دریافت این کدها معمولا از APIهای سایت SymbolSource.org استفاده میکند. البته منبع اصلی تمام کدهای دات نت فریمورک همان referencesource.microsoft.com است.
کلاس ResXResourceProviderFactory
پیادهسازی پیشفرض کلاس ResourceProviderFactory در ASP.NET که در کلاس ResXResourceProviderFactory قرار دارد، به صورت زیر است:
// Type: System.Web.Compilation.ResXResourceProviderFactory // Assembly: System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a // Assembly location: C:\Windows\Microsoft.NET\assembly\GAC_32\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll using System.Runtime; using System.Web; namespace System.Web.Compilation { internal class ResXResourceProviderFactory : ResourceProviderFactory { [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public ResXResourceProviderFactory() { } public override IResourceProvider CreateGlobalResourceProvider(string classKey) { return (IResourceProvider) new GlobalResXResourceProvider(classKey); } public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { return (IResourceProvider) new LocalResXResourceProvider(VirtualPath.Create(virtualPath)); } } }
در این کلاس برای تولید پرووایدر منابع محلی از کلاس VirtualPath استفاده شده است که امکاناتی جهت استخراج مسیرهای موردنظر با توجه به مسیر نسبی و مجازی ارائهشده فراهم میکند. متاسفانه این کلاس نیز با سطح دسترسی internal تعریف شده است و امکان استفاده مستقیم از آن وجود ندارد.
کلاس GlobalResXResourceProvider
پیادهسازی پیشفرض اینترفیس IResourceProvider در ASP.NET برای منابع کلی که در کلاس GlobalResXResourceProvider قرار دارد، به صورت زیر است:
internal class GlobalResXResourceProvider : BaseResXResourceProvider { private string _classKey; internal GlobalResXResourceProvider(string classKey) { _classKey = classKey; } protected override ResourceManager CreateResourceManager() { string fullClassName = BaseResourcesBuildProvider.DefaultResourcesNamespace + "." + _classKey; // If there is no app resource assembly, return null if (BuildManager.AppResourcesAssembly == null) return null; ResourceManager resourceManager = new ResourceManager(fullClassName, BuildManager.AppResourcesAssembly); resourceManager.IgnoreCase = true; return resourceManager; } public override IResourceReader ResourceReader { get { // App resources don't support implicit resources, so the IResourceReader should never be needed throw new NotSupportedException(); } } }
در این کلاس عملیات تولید نمونه مناسب از کلاس ResourceManager انجام میشود. مقدار BaseResourcesBuildProvider.DefaultResourcesNamespace به صورت زیر تعریف شده است:
internal const string DefaultResourcesNamespace = "Resources";
پارامتر classKey درواقع اشاره به نام فایل اصلی منبع کلی دارد. مثلا اگر این مقدار برابر Resource1 باشد، کلاس ResourceManager برای نوع داده Resources.Resource1 تولید خواهد شد.
همچنین اسمبلی موردنظر برای یافتن ورودیهای منابع کلی که از BuildManager.AppResourcesAssembly دریافت شده است، به صورت پیش فرض همنام با مسیر منابع کلی و با عنوان App_GlobalResources تولید میشود.
کلاس BuildManager فرایندهای کامپایل کدها و صفحات برای تولید اسمبلیها و نگهداری از آنها در حافظه را مدیریت میکند. این کلاس که محتوای نسبتا مفصلی دارد (نزدیک به 2000 خط کد) به صورت public و sealed تعریف شده است. بنابراین با ریفرنس دادن اسمبلی System.Web در فضای نام System.Web.Compilation در دسترس است، اما نمیتوان کلاسی از آن مشتق کرد. BuildManager حاوی تعداد زیادی اعضای استاتیک برای دسترسی به اطلاعات اسمبلیهاست. اما متاسفانه بیشتر آنها سطح دسترسی عمومی ندارند.
نکته: همانطور که در بالا نیز اشاره شد، ازآنجاکه کلاس ResourceReader در اینجا تنها برای عبارات بومی سازی ضمنی کاربرد دارد، و نیز عبارات بومیسازی ضمنی تنها برای منابع محلی کاربرد دارند، در این کلاس برای خاصیت مربوطه در پیاده سازی اینترفیس IResourceProvider یک خطای عدم پشتیبانی (NotSupportedException) صادر شده است.
کلاس LocalResXResourceProvider
پیادهسازی پیشفرض اینترفیس IResourceProvider در ASP.NET برای منابع محلی که در کلاس LocalResXResourceProvider قرار دارد، به صورت زیر است:
internal class LocalResXResourceProvider : BaseResXResourceProvider { private VirtualPath _virtualPath; internal LocalResXResourceProvider(VirtualPath virtualPath) { _virtualPath = virtualPath; } protected override ResourceManager CreateResourceManager() { ResourceManager resourceManager = null; Assembly pageResAssembly = GetLocalResourceAssembly(); if (pageResAssembly != null) { string fileName = _virtualPath.FileName; resourceManager = new ResourceManager(fileName, pageResAssembly); resourceManager.IgnoreCase = true; } else { throw new InvalidOperationException(SR.GetString(SR.ResourceExpresionBuilder_PageResourceNotFound)); } return resourceManager; } public override IResourceReader ResourceReader { get { // Get the local resource assembly for this page Assembly pageResAssembly = GetLocalResourceAssembly(); if (pageResAssembly == null) return null; // Get the name of the embedded .resource file for this page string resourceFileName = _virtualPath.FileName + ".resources"; // Make it lower case, since GetManifestResourceStream is case sensitive resourceFileName = resourceFileName.ToLower(CultureInfo.InvariantCulture); // Get the resource stream from the resource assembly Stream resourceStream = pageResAssembly.GetManifestResourceStream(resourceFileName); // If this page has no resources, return null if (resourceStream == null) return null; return new ResourceReader(resourceStream); } } [PermissionSet(SecurityAction.Assert, Unrestricted = true)] private Assembly GetLocalResourceAssembly() { // Remove the page file name to get its directory VirtualPath virtualDir = _virtualPath.Parent; // Get the name of the local resource assembly string cacheKey = BuildManager.GetLocalResourcesAssemblyName(virtualDir); BuildResult result = BuildManager.GetBuildResultFromCache(cacheKey); if (result != null) { return ((BuildResultCompiledAssembly)result).ResultAssembly; } return null; } }
عملیات موجود در این کلاس باتوجه به فرایندهای مربوط به یافتن اسمبلی مربوطه با استفاده از مسیر ارائهشده، کمی پیچیدهتر از کلاس قبلی است.
در متد GetLocalResourceAssembly عملیات یافتن اسمبلی متناظر با درخواست جاری انجام میشود. اینکار باتوجه به نحوه نامگذاری اسمبلی منابع محلی که در ابتدای این مطلب اشاره شد انجام میشود. مثلا اگر صفحه درخواستی در مسیر SubDir1/Page1.aspx/~ باشد، در این متد با استفاده از ابزارهای موجود عنوان اسمبلی نهایی برای این مسیر که به صورت App_LocalResources.SubDir1.XXXXX است تولید و درنهایت اسمبلی مربوطه استخراج میشود.
درضمن در اینجا هم کلاس ResourceManager برای نوع داده متناظر با نام فایل اصلی منبع محلی تولید میشود. مثلا برای مسیر مجازی SubDir1/Page1.aspx/~ نوع دادهای با نام Page1.aspx درنظر گرفته خواهد شد (با توجه به نام فایل منبع محلی که باید به صورت Page1.aspx.resx باشد. در قسمت قبل در این باره شرح داده شده است).
نکته: کلاس SR (مخفف String Resources) که در فضای نام System.Web قرار دارد، حاوی عناوین کلیدهای Resourceهای مورداستفاده در اسمبلی System.Web است. این کلاس با سطح دسترسی internal و به صورت sealed تعریف شده است. عنوان تمامی کلیدها به صورت ثوابتی از نوع رشته تعریف شدهاند.
SR درواقع یک Wrapper بر روی کلاس ResourceManager است تا از تکرار عناوین کلیدهای منابع که از نوع رشته هستند، در جاهای مختلف برنامه جلوگیری شود. کار این کلاس مشابه کاری است که کتابخانه T4MVC برای نگهداری عناوین کنترلرها و اکشنها به صورت رشتههای ثابت انجام میدهد. از این روش در جای جای دات نت فریمورک برای نگهداری رشتههای ثابت استفاده شده است!
نکته: باتوجه به استفاده از عبارات بومیسازی ضمنی در استفاده از ورودیهای منابع محلی، خاصیت ResourceReader در این کلاس نمونهای متناظر برای درخواست جاری از کلاس ResourceReader با استفاده از Stream استخراج شده از اسمبلی یافته شده، تولید میکند.
کلاس پایه BaseResXResourceProvider
کلاس پایه BaseResXResourceProvider که در دو پیادهسازی نشان داده شده در بالا استفاده شده است (هر دو کلاس از این کلاس مشتق شدهاند)، به صورت زیر است:
internal abstract class BaseResXResourceProvider : IResourceProvider { private ResourceManager _resourceManager; ///// IResourceProvider implementation public virtual object GetObject(string resourceKey, CultureInfo culture) { // Attempt to get the resource manager EnsureResourceManager(); // If we couldn't get a resource manager, return null if (_resourceManager == null) return null; if (culture == null) culture = CultureInfo.CurrentUICulture; return _resourceManager.GetObject(resourceKey, culture); } public virtual IResourceReader ResourceReader { get { return null; } } ///// End of IResourceProvider implementation protected abstract ResourceManager CreateResourceManager(); private void EnsureResourceManager() { if (_resourceManager != null) return; _resourceManager = CreateResourceManager(); } }
نکته: دقت کنید که در کد بالا درصورت فراهم نکردن مقداری برای کالچر، از کالچر UI در ثرد جاری (CultureInfo.CurrentUICulture) به عنوان مقدار پیشفرض استفاده میشود.
کلاس ResourceManager
در زمان اجرا ASP.NET کلید مربوط به منبع موردنظر را با استفاده از کالچر جاری UI انتخاب میکند. در قسمت اول این سری مطالب شرح کوتاهی بابت انواع کالچرها داده شد، اما برای توضیحات کاملتر به اینجا مراجعه کنید.
در ASP.NET به صورت پیشفرض تمام منابع در زمان اجرا از طریق نمونهای از کلاس ResourceManager در دسترس خواهند بود. به ازای هر نوع Resource که درخواستی برای یک کلید آن ارسال میشود یک نمونه از کلاس ResourceManager ساخته میشود. در این هنگام (یعنی پس از اولین درخواست به کلیدهای یک منبع) اسمبلی ستلایت مناسب آن پس از یافته شدن (یا تولیدشدن در زمان اجرا) به دامین ASP.NET جاری بارگذاری میشود و تا زمانیکه این دامین Unload نشود در حافظه سرور باقی خواهد ماند.
نکته: کلاس ResourceManager تنها توانایی استخراج کلیدهای Resource از اسمبلیهای ستلایتی (فایلهای resources. که در قسمت اول به آنها اشاره شد) که در AppDomain جاری بارگذاری شدهاند را دارد.
کلاس ResourceManager به صورت زیر نمونه سازی میشود:
System.Resources.ResourceManager(string baseName, Assembly assemblyName)
پارامتر baseName به نام کامل ریشه اسمبلی اصلی موردنظر(با فضای نام و ...) اما بدون پسوند اسمبلی مربوطه (resources.) اشاره دارد. این نام که برابر نام کلاس نهایی تولیدشده برای منبع موردنظر است همنام با فایل اصلی و پیشفرض منبع (فایلی که حاوی عنوان هیچ زبان و کالچری نیست) تولید میشود. مثلا برای اسمبلی ستلایت با عنوان MyApplication.MyResource.fa-IR.resources باید از عبارت MyApplication.MyResource استفاده شود.
پارامتر assemblyName نیز به اسمبلی حاوی اسمبلی ستلایت اصلی اشاره دارد. درواقع همان اسمبلی اصلی که نوع داده مربوط به فایل منبع اصلی درون آن embed شده است.
مثلا:
var manager = new System.Resources.ResourceManager("Resources.Resource1", typeof(Resource1).Assembly)
یا
var manager = new System.Resources.ResourceManager("Resources.Resource1", Assembly.LoadFile(@"c:\MyResources\MyGlobalResources.dll"))
روش دیگری نیز برای تولید نمونهای از این کلاس وجود دارد که با استفاده از متد استاتیک زیر که در خود کلاس ResourceManager تعریف شده است انجام میشود:
public static ResourceManager CreateFileBasedResourceManager(string baseName, string resourceDir, Type usingResourceSet)
در این متد کار استخراج ورودیهای منابع مستقیما از فایلهای resources. انجام میشود. در اینجا baseName نام فایل اصلی منبع بدون پیشوند resources. است. resourceDir نیز مسیری است که فایلهای resources. در آن قرار دارند. usingResourceSet نیز نوع کلاس سفارشی سازی شده از ResourceSet برای استفاده به جای کلاس پیشفرض است که معمولا مقدار null برای آن وارد میشود تا از همان کلاس پیشفرض استفاده شود (چون برای بیشتر نیازها همین کلاس پیشفرض کفایت میکند).
نکته: برای تولید فایل resources. از یک فایل resx. میتوان از ابزار resgen همانند زیر استفاده کرد:
resgen d:\MyResources\MyResource.fa.resx
نکته: عملیاتی که درون کلاس ResourceManager انجام میشود پیچیدهتر از آن است که به نظر میآید. این عملیات شامل فرایندهای بسیاری شامل بارگذاری کلیدهای مختلف یافته شده و مدیریت ذخیره موقت آنها در حافظه (کش)، کنترل و مدیریت انواع Resource Setها، و مهمتر از همه مدیریت عملیات Fallback و ... که در نهایت شامل هزاران خط کد است که با یک جستجوی ساده قابل مشاهده و بررسی است (^).
نمونهسازی مناسب از ResourceManager
در کدهای نشان داده شده در بالا برای پیادهسازی پیشفرض در ASP.NET، مهمترین نکته همان تولید نمونه مناسب از کلاس ResourceManager است. پس از آماده شدن این کلاس عملیات استخراج ورودیهای منابع براحتی و با مدیریت کامل انجام میشود. اما ازآنجاکه تقریبا تمامی APIهای موردنیاز با سطح دسترسی internal تعریف شدهاند، متاسفانه تهیه و تولید این نمونه مناسب خارج از اسمبلی System.Web به صورت مستقیم وجود ندارد.
درهرصورت، برای آشنایی بیشتر با فرایند نشان داده شده، تولید این نمونه مناسب و استفاده مستقیم از آن میتواند مفید و نیز جالب باشد. پس از کمی تحقیق و با استفاده از Reflection به کدهای زیر رسیدم:
private ResourceManager CreateGlobalResourceManager(string classKey) { var baseName = "Resources." + classKey; var buildManagerType = typeof(BuildManager); var property = buildManagerType.GetProperty("AppResourcesAssembly", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField); var appResourcesAssembly = (Assembly)property.GetValue(null, null); return new ResourceManager(baseName, appResourcesAssembly) { IgnoreCase = true }; }
تنها نکته کد فوق دسترسی به اسمبلی منابع کلی در خاصیت AppResourcesAssembly از کلاس BuildManager با استفاده از BindingFlagهای نشان داده شده است.
نحوه استفاده از این متد هم به صورت زیر است:var manager = CreateGlobalResourceManager("Resource1"); Label1.Text = manager.GetString("String1");
private ResourceManager CreateLocalResourceManager(string virtualPath) { var virtualPathType = typeof(BuildManager).Assembly.GetType("System.Web.VirtualPath", true); var virtualPathInstance = Activator.CreateInstance(virtualPathType, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { virtualPath }, CultureInfo.InvariantCulture); var buildResultCompiledAssemblyType = typeof(BuildManager).Assembly.GetType("System.Web.Compilation.BuildResultCompiledAssembly", true); var propertyResultAssembly = buildResultCompiledAssemblyType.GetProperty("ResultAssembly", BindingFlags.NonPublic | BindingFlags.Instance); var methodGetLocalResourcesAssemblyName = typeof(BuildManager).GetMethod("GetLocalResourcesAssemblyName", BindingFlags.NonPublic | BindingFlags.Static); var methodGetBuildResultFromCache = typeof(BuildManager).GetMethod("GetBuildResultFromCache", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(string) }, null); var fileNameProperty = virtualPathType.GetProperty("FileName"); var virtualPathFileName = (string)fileNameProperty.GetValue(virtualPathInstance, null); var parentProperty = virtualPathType.GetProperty("Parent"); var virtualPathParent = parentProperty.GetValue(virtualPathInstance, null); var localResourceAssemblyName = (string)methodGetLocalResourcesAssemblyName.Invoke(null, new object[] { virtualPathParent }); var buildResultFromCache = methodGetBuildResultFromCache.Invoke(null, new object[] { localResourceAssemblyName }); Assembly localResourceAssembly = null; if (buildResultFromCache != null) localResourceAssembly = (Assembly)propertyResultAssembly.GetValue(buildResultFromCache, null); if (localResourceAssembly == null) throw new InvalidOperationException("Unable to find the matching resource file."); return new ResourceManager(virtualPathFileName, localResourceAssembly) { IgnoreCase = true }; }
ازجمله نکات مهم این متد تولید یک نمونه از کلاس VirtualPath برای Parse کردن مسیر مجازی واردشده برای صفحه درخواستی است. از این کلاس برای بدست آوردن نام فایل منبع محلی به همراه مسیر فولدر مربوطه جهت استخراج اسمبلی متناظر استفاده میشود.
نکته مهم دیگر این کد دسترسی به متد GetLocalResourcesAssemblyName از کلاس BuildManager است که با استفاده از مسیر فولدر مربوط به صفحه درخواستی نام اسمبلی منبع محلی مربوطه را برمیگرداند.
درنهایت با استفاده از متد GetBuildResultFromCache از کلاس BuildManager اسمبلی موردنظر بدست میآید. همانطور که از نام این متد برمیآید این اسمبلی از کش خوانده میشود. البته مدیریت این اسمبلیها کاملا توسط BuildManager و سایر ابزارهای موجود در ASP.NET انجام خواهد شد.
نحوه استفاده از متد فوق نیز به صورت زیر است:
var manager = CreateLocalResourceManager("~/Default.aspx"); Label1.Text = manager.GetString("Label1.Text");
نکته: ارائه و شرح کدهای پیادهسازیهای پیشفرض برای آشنایی با نحوه صحیح سفارشی سازی این کلاسها آورده شده است. پس با دقت بیشتر بر روی این کدها سعی کنید نحوه پیادهسازی مناسب را برای سفارشیسازی موردنظر خود پیدا کنید.
تا اینجا با مقدمات فرایند تولید پرووایدرهای سفارشی برای استفاده در فرایند بارگذاری ورودیهای Resourceها آشنا شدیم. در ادامه به بحث تولید پرووایدرهای سفارشی برای استفاده از دیگر انواع منابع (به غیر از فایلهای resx.) خواهم پرداخت.
منابع:
پس از ارتقاء Angular CLI و ساختار پروژهی قبلی خود به نگارش 6، اولین موردی را که مشاهده خواهید کرد، این است: برنامه دیگر کامپایل نمیشود! اولین دلیل آن عدم استفادهی از HttpClient معرفی شدهی در نگارش 4.3 است و دومین دلیل مهم آن، تغییرات بنیادین RxJS است که خلاصهی کاربردی آنرا در این مطلب بررسی خواهیم کرد.
RxJS اکنون جزئی از پروژههای گوگل است
توسعه دهندهی اصلی RxJS یا همان Ben Lesh اکنون به گوگل پیوستهاست و جزو تیم Angular است. بنابراین در آینده شاهد یکپارچگی بهتر این دو با هم خواهیم بود. البته RxJS هنوز هم به عنوان یک پروژهی مستقل از Angular مدیریت خواهد شد.
آشنایی با تغییرات RxJS 5.5 جهت مهاجرت به RxJS 6.0 ضروری است
در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» با pipe-able operators آشنا شدیم و این موارد پایههای مهاجرت به RxJS 6.0 هستند. بنابراین پیش از مطالعهی ادامهی بحث نیاز است این مطلب را به خوبی مطالعه و بررسی کنید.
تغییر رفتار خطاهای مدیریت نشده در RxJS 6.0
تا پیش از RxJS 6.0 اگر خطای مدیریت نشدهای رخ میداد، این خطا به صورت synchronous به فراخوان صادر میشد. این رفتار در نگارش 6 تغییر کرده و صدور آن اینبار asynchronous شدهاست.
برای مثال یک چنین کدی تا پیش از RxJS 6.0 کار میکرد:
از این جهت که دومین پارامتر در اینجا، متدی است که کار مدیریت خطا را انجام میدهد و چون ذکر نشدهاست، خطای رخ دادهی حاصل، به صورت همزمان به فراخوان صادر شدهاست و try/catch نیز در اینجا کار میکند. اما این مثال دیگر در نگارش 6 کار نخواهد کرد و صدور خطای مدیریت نشده، دیگر همزمان نیست و قسمت catch این قطعه کد دیگر هیچگاه فراخوانی نمیشود. البته این رفتار طبیعی است که میبایستی از نگارشهای پیشین، با Observable هایی که عموما async هستند، وجود میداشت و اکنون اصلاح شدهاست.
برای اصلاح این کد در نگارش 6، همان پارامتر دوم متد را مقدار دهی کنید و try/catch را در صورت وجود حذف نمائید.
تغییرات مهم importها در RxJS 6.0
همانطور که در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» نیز بررسی کردیم، تا نگارش 5 این کتابخانه، importها به صورت زیر بودند:
و پس از RxJS 5.5 امکان import آنها با روش مخصوص ES 6 میسر شدهاست:
هر چند نگارش 5.5 بهبودهای قابل ملاحظهای و مزایایی مانند حذف سادهتر کدهای مرده، عدم تعریف عملگرها به صورت استاتیک و همچنین سازگاری بهتر با ابزارهایی مانند TSLint را به همراه دارد، اما باز هم دست آخر به تعداد زیادی import مانند کدهای زیر میرسیدیم:
این مشکل در RxJS 6.0 برطرف شدهاست و در ماژولهای مختلف برنامه حداکثر به دو سطر خلاصه شدهی زیر نیاز خواهیم داشت:
در نگارش 6، تمام «نوعها» مانند Observable و Subject و «متدهای ایجاد» مانند timer و interval از مسیر rxjs دریافت میشوند و تمام «عملگرها» مانند map و filter از مسیر rxjs/operators اضافه خواهند شد و ... همین!
البته RxJS 6.0 در کل به همراه 4 گروه کلی importها است که در زیر مشاهده میکنید (در اینجا مواردی که کمتر در برنامههای Angular به صورت مستقیم استفاده میشوند مانند ajax آن و یا webSocket هم قابل مشاهده هستند):
مواردی که از RxJS 6.0 حذف شدهاند
برای کاهش حجم کتابخانهی RxJS و همچنین جلوگیری از بکارگیری متدهایی که نمیبایستی خارج از کدهای اصلی خود RxJS استفاده شوند، تعداد زیادی از متدهای قدیمی آن و روشهای کار پیشین با RxJS حذف شدهاند. برای مثال شما در RxJS 5.5 میتوانید برای کار با عملگر of، یا آنرا از مسیر rxjs/add/observable/of دریافت کنید (همان روش وصله کردن تا پیش از RxJS 5.5) و یا آنرا از مسیر rxjs/observable/of به روش مخصوص ES 6.0 به برنامه اضافه کنید و یا حتی امکان دریافت آن از مسیر rxjs/observable/fromArray نیز میسر است.
در RxJS 6.0 تمام اینها حذف شدهاند و فقط روش زیر باقی ماندهاست:
به این ترتیب نه فقط حجم این کتابخانه کاهش یافتهاست، بلکه با کاهش سطح API آن، یادگیری آن نیز سادهتر شدهاست.
معرفی بستهی rxjs-compat
در مطلب «ارتقاء به Angular 6: بررسی تغییرات Angular CLI» روش ارتقاء وابستگیهای پروژه به نگارش 6 را بررسی کردیم. یکی از مراحل آن اجرای دستور زیر بود:
این مورد صرفا وابستگی rxjs ذکر شدهی در فایل package.json را به آخرین نگارش آن به روز رسانی و همچنین نصب میکند.
پس از آن اگر پروژه را کامپایل کنید، پر خواهد بود از خطاهای rxjs، مانند:
این مشکل با بستههای ثالثی وجود دارند که هنوز از نگارشهای قبلی RxJS استفاده میکنند و همانطور که عنوان شد، RxJS 6.0 شامل حذفیات و نقل و انتقالات بسیاری است. به همین جهت هیچکدام از کتابخانههای ثالث مبتنی بر نگارشهای پیشین RxJS دیگر کار نکرده و کامپایل نخواهند شد.
برای رفع این مشکل و ارائهی راهحلی کوتاه مدت، بستهای به نام rxjs-compat ارائه شدهاست که سبب هدایت تعاریف قدیمی به تعاریف جدید میشود و به این ترتیب کدهای کتابخانهی ثالث، بدون مشکل با نگارش 6 نیز قابل استفاده خواهند بود.
برای نصب آن نیاز است دستور زیر را صادر کنید:
اکنون اگر برنامه را مجددا کامپایل کنید، تمام خطاهای مرتبط با کتابخانههای ثالث مورد استفاده برطرف شدهاند.
البته دقت داشته باشید از rxjs-compat به عنوان یک راه حل موقت باید استفاده کرد و نیاز است ابتدا کدهای خود را به روش pipe-able operators بازنویسی کنید و مسیرهای importها را اصلاح کنید و در آخر بستههای جدید وابستگیهای ثالث را که از RxJS6 استفاده میکنند، نصب نمائید. در نهایت rxjs-compat را حذف کنید.
خودکار سازی اصلاح importها در برنامههای پیشین، جهت مهاجرت به RxJS 6.0
با توجه به این تغییرات و حذف و اضافه شدنها در نگارش 6، تقریبا دیگر هیچکدام از importهای قبلی شما کار نمیکنند! و اصلاح آنها نیاز به زمان زیادی خواهد داشت. به همین جهت تیم RxJS ابزاری را طراحی کردهاند که با اجرای آن بر روی پروژه، به صورت خودکار تمام importهای قبلی را به نگارش جدید تبدیل میکند. برای اینکار ابتدا ابزار rxjs-tslint را نصب کنید:
در ادامه فایل tslint.json پروژه خود را گشوده و مداخل زیر را به آن اضافه و ویرایش نمائید:
سپس به ریشهی پروژهی خود وارد شده و دستور زیر را اجرا کنید:
کیفیت کار این ابزار تا حدی است که تیمهای داخلی گوگل از آن برای ارتقاء کدهای پیشین استفاده میکنند و بر روی پروژههای واقعی آزمایش شدهاست.
البته توصیه شدهاست این ابزار را بیش از یکبار نیاز است اجرا کنید.
خلاصهی روش مهاجرت به RxJS 6x
ابتدا آخرین نگارش rxjs را نصب کنید:
سپس rxjs-compat را جهت رفع کمبودهای کتابخانههای ثالث مورد استفاده نصب نمائید:
پس از آن ابزار rxjs-tslint را نصب و اجرا کنید:
و در آخر ارتقاء به روش pipe-able را باید مدنظر داشته باشید.
یافتن معادلهای جدید دستورات قدیمی
در حین تبدیل کدهای قدیمی به جدید نیاز خواهید داشت تا معادلها را بیابید. برای این منظور به مستندات رسمی این مهاجرت مراجعه کنید:
https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md
برای مثال در اینجا مشاهده خواهید کرد که معادل Observable.throw حذف شده، اکنون throwError است و همینطور برای مابقی.
یک مثال واقعی تغییر یافته
مخزن کد تمام مثالهای سایت جاری که پیشتر منتشر شدهاند، به نسخهی 6 ارتقاء داده شد. ریز تغییرات RxJS 6.0 آنها را در اینجا میتوانید مشاهده کنید.
RxJS اکنون جزئی از پروژههای گوگل است
توسعه دهندهی اصلی RxJS یا همان Ben Lesh اکنون به گوگل پیوستهاست و جزو تیم Angular است. بنابراین در آینده شاهد یکپارچگی بهتر این دو با هم خواهیم بود. البته RxJS هنوز هم به عنوان یک پروژهی مستقل از Angular مدیریت خواهد شد.
آشنایی با تغییرات RxJS 5.5 جهت مهاجرت به RxJS 6.0 ضروری است
در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» با pipe-able operators آشنا شدیم و این موارد پایههای مهاجرت به RxJS 6.0 هستند. بنابراین پیش از مطالعهی ادامهی بحث نیاز است این مطلب را به خوبی مطالعه و بررسی کنید.
تغییر رفتار خطاهای مدیریت نشده در RxJS 6.0
تا پیش از RxJS 6.0 اگر خطای مدیریت نشدهای رخ میداد، این خطا به صورت synchronous به فراخوان صادر میشد. این رفتار در نگارش 6 تغییر کرده و صدور آن اینبار asynchronous شدهاست.
برای مثال یک چنین کدی تا پیش از RxJS 6.0 کار میکرد:
try { source$.subscribe(nextFn, undefined, completeFn); } catch (err) { handleError(err); }
برای اصلاح این کد در نگارش 6، همان پارامتر دوم متد را مقدار دهی کنید و try/catch را در صورت وجود حذف نمائید.
تغییرات مهم importها در RxJS 6.0
همانطور که در مطلب «کاهش حجم قابل ملاحظهی برنامههای Angular با استفاده از RxJS 5.5» نیز بررسی کردیم، تا نگارش 5 این کتابخانه، importها به صورت زیر بودند:
import 'rxjs/add/operator/map';
import { map } from 'rxjs/operators';
import { timer } from 'rxjs/observable/timer'; import { of } from 'rxjs/observable/of'; import { from } from 'rxjs/observable/from'; import { range } from 'rxjs/observable/range';
import { interval, of } from 'rxjs'; import { filter, mergeMap, scan } from 'rxjs/operators';
البته RxJS 6.0 در کل به همراه 4 گروه کلی importها است که در زیر مشاهده میکنید (در اینجا مواردی که کمتر در برنامههای Angular به صورت مستقیم استفاده میشوند مانند ajax آن و یا webSocket هم قابل مشاهده هستند):
rxjs rxjs/operators rxjs/testing rxjs/webSocket rxjs/ajax
مواردی که از RxJS 6.0 حذف شدهاند
برای کاهش حجم کتابخانهی RxJS و همچنین جلوگیری از بکارگیری متدهایی که نمیبایستی خارج از کدهای اصلی خود RxJS استفاده شوند، تعداد زیادی از متدهای قدیمی آن و روشهای کار پیشین با RxJS حذف شدهاند. برای مثال شما در RxJS 5.5 میتوانید برای کار با عملگر of، یا آنرا از مسیر rxjs/add/observable/of دریافت کنید (همان روش وصله کردن تا پیش از RxJS 5.5) و یا آنرا از مسیر rxjs/observable/of به روش مخصوص ES 6.0 به برنامه اضافه کنید و یا حتی امکان دریافت آن از مسیر rxjs/observable/fromArray نیز میسر است.
در RxJS 6.0 تمام اینها حذف شدهاند و فقط روش زیر باقی ماندهاست:
import { of } from 'rxjs';
معرفی بستهی rxjs-compat
در مطلب «ارتقاء به Angular 6: بررسی تغییرات Angular CLI» روش ارتقاء وابستگیهای پروژه به نگارش 6 را بررسی کردیم. یکی از مراحل آن اجرای دستور زیر بود:
ng update rxjs
پس از آن اگر پروژه را کامپایل کنید، پر خواهد بود از خطاهای rxjs، مانند:
ERROR in node_modules/ng2-slim-loading-bar/src/slim-loading-bar.service.d.ts(1,10): error TS2305: Module '"/node_modules/rxjs/Observable"' has no exported member 'Observable'.
برای رفع این مشکل و ارائهی راهحلی کوتاه مدت، بستهای به نام rxjs-compat ارائه شدهاست که سبب هدایت تعاریف قدیمی به تعاریف جدید میشود و به این ترتیب کدهای کتابخانهی ثالث، بدون مشکل با نگارش 6 نیز قابل استفاده خواهند بود.
برای نصب آن نیاز است دستور زیر را صادر کنید:
npm i rxjs-compat --save
البته دقت داشته باشید از rxjs-compat به عنوان یک راه حل موقت باید استفاده کرد و نیاز است ابتدا کدهای خود را به روش pipe-able operators بازنویسی کنید و مسیرهای importها را اصلاح کنید و در آخر بستههای جدید وابستگیهای ثالث را که از RxJS6 استفاده میکنند، نصب نمائید. در نهایت rxjs-compat را حذف کنید.
خودکار سازی اصلاح importها در برنامههای پیشین، جهت مهاجرت به RxJS 6.0
با توجه به این تغییرات و حذف و اضافه شدنها در نگارش 6، تقریبا دیگر هیچکدام از importهای قبلی شما کار نمیکنند! و اصلاح آنها نیاز به زمان زیادی خواهد داشت. به همین جهت تیم RxJS ابزاری را طراحی کردهاند که با اجرای آن بر روی پروژه، به صورت خودکار تمام importهای قبلی را به نگارش جدید تبدیل میکند. برای اینکار ابتدا ابزار rxjs-tslint را نصب کنید:
npm i -g rxjs-tslint
{ "rulesDirectory": [ "node_modules/rxjs-tslint" ], "rules": { "rxjs-collapse-imports": true, "rxjs-pipeable-operators-only": true, "rxjs-no-static-observable-methods": true, "rxjs-proper-imports": true } }
rxjs-5-to-6-migrate -p src/tsconfig.app.json
البته توصیه شدهاست این ابزار را بیش از یکبار نیاز است اجرا کنید.
خلاصهی روش مهاجرت به RxJS 6x
ابتدا آخرین نگارش rxjs را نصب کنید:
ng update rxjs
npm i rxjs-compat --save
npm i -g rxjs-tslint rxjs-5-to-6-migrate -p src/tsconfig.app.json
یافتن معادلهای جدید دستورات قدیمی
در حین تبدیل کدهای قدیمی به جدید نیاز خواهید داشت تا معادلها را بیابید. برای این منظور به مستندات رسمی این مهاجرت مراجعه کنید:
https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md
برای مثال در اینجا مشاهده خواهید کرد که معادل Observable.throw حذف شده، اکنون throwError است و همینطور برای مابقی.
یک مثال واقعی تغییر یافته
مخزن کد تمام مثالهای سایت جاری که پیشتر منتشر شدهاند، به نسخهی 6 ارتقاء داده شد. ریز تغییرات RxJS 6.0 آنها را در اینجا میتوانید مشاهده کنید.