ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; var options = new OidcClientOptions(); options.BackchannelHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, certificate, chain, sslPolicyErrors) => true };
public class Product { public int ProductId { get; set; } public string ProductName { get; set; } public long ProductPrice { get; set; } }
public class ProductMetadata : MetadataClass<Product> { public ProductMetadata () { this.Validation(x => x.ProductName).Required("عنوان محصول وارد نشده است"); this.Validation(x => x.ProductPrice).Range(1000,100000,"قیمت محصول باید بین هزار تومان تا صدهزار تومان باشد"); } }
public class FluentMetadataConfiguration : IFluentMetadataConfiguration { public void OnTypeCreation(MetadataContainer metadataContainer) { metadataContainer.Add(new ProductMetadata()); } }
[EnableClientAccess()] [FluentMetadata(typeof(FluentMetadataConfiguration))] public class FluentMetadataTestDomainService : DomainService { ... }
Domain Driven Design: The Good Parts
The greenfield project started out so promising. Instead of devolving into big ball of mud, the team decided to apply domain-driven design principles. Ubiquitous language, proper boundaries, encapsulation, it all made sense.
But along the way, something went completely and utterly wrong. It started with arguments on the proper way of implementing aggregates and entities. Arguments began over project and folder structure. Someone read a blog post that repositories are evil, and ORMs the devil incarnate. Another read that relational databases are last century, we need to store everything as a stream of events. Then came the actor model and frameworks that sounded like someone clearing their throat. Instead of a nice, clean architecture, the team chased the next new approach without ever actually shipping anything.
Beyond the endless technical arguments it causes, domain-driven design can actually produce great software. We have to look past the hype into the true value of DDD, what it can bring to our organizations and how it can enable us to build quality systems. With the advent of microservices, DDD is more important than ever - but only if we can get to the good parts.
مقدمهای بر NET MAUI.
An Introduction to .NET MAUI For Mobile Development
.NET Multi-platform App UI (.NET MAUI) is a cross-platform framework for creating native mobile and desktop apps with C# and XAML.
.NET MAUI is open-source and is the evolution of Xamarin.Forms, extended from mobile to desktop scenarios, with UI controls rebuilt from the ground up for performance and extensibility. If you've previously used Xamarin.Forms to build cross-platform user interfaces, you'll notice many similarities with .NET MAUI. However, there are also some differences. Using .NET MAUI, you can create multi-platform apps using a single project, but you can add platform-specific source code and resources if necessary. One of the key aims of .NET MAUI is to enable you to implement as much of your app logic and UI layout as possible in a single code-base.
0:00 - Setup Visual Studio and MAUI Project
00:16:25 - Create MAUI Pages with C#
00:27:42 - Create MAUI Pages with XAML
00:32:28 - Explore MAUI Layouts
00:39:38 - Static Shared Resources
00:44:36 - Platform Specific Values
00:50:11 - Page Navigation
مدل برنامه
در ابتدای کار نیاز است تا ساختاری را جهت ارائه لیستی از مطالب که دارای گزینه امتیاز دهی میباشند، تهیه کنیم:
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
MVC Scaffolding #2
دو نوع پارامتر حین کار با MVC Scaffolding مهیا هستند:
الف) سوئیچها
مانند پارامترهای boolean عمل کرده و شامل موارد ذیل میباشند. تمام این پارامترها به صورت پیش فرض دارای مقدار false بوده و ذکر هرکدام در دستور نهایی سبب true شدن مقدار آنها میگردد:
Repository: برای تولید کدها بر اساس الگوی مخزن
Force: برای بازنویسی فایلهای موجود.
ReferenceScriptLibraries: ارجاعاتی را به اسکریپتهای موجود در پوشه Scripts، اضافه میکند.
NoChildItems: در این حالت فقط کلاس کنترلر تولید میشود و از سایر ملحقات مانند تولید Viewها، DbContext و غیره صرفنظر خواهد شد.
ب) رشتهها
این نوع پارامترها، رشتهای را به عنوان ورودی خود دریافت میکنند و شامل موارد ذیل هستند:
ControllerName: جهت مشخص سازی نام کنترلر مورد نظر
ModelType: برای ذکر صریح کلاس مورد استفاده در تشکیل کنترلر بکار میرود. اگر ذکر نشود، از نام کنترلر حدس زده خواهد شد.
DbContext: نام کلاس DbContext تولیدی را مشخص میکند. اگر ذکر نشود از نامی مانند ProjectNameContex استفاده خواهد کرد.
Project: پیش فرض آن پروژه جاری است یا اینکه میتوان پروژه دیگری را برای قرار دادن فایلهای تولیدی مشخص کرد. (برای مثال هربار یک سری کد مقدماتی را در یک پروژه جانبی تولید کرد و سپس موارد مورد نیاز را از آن به پروژه اصلی افزود)
CodeLanguage: میتواند cs یا vb باشد. پیش فرض آن زبان جاری پروژه است.
Area: اگر میخواهید کدهای تولیدی در یک ASP.NET MVC area مشخص قرار گیرند، نام Area مشخصی را در اینجا ذکر کنید.
Layout: در حالت پیش فرض از فایل layout اصلی استفاده خواهد شد. اما اگر نیاز است از layout دیگری استفاده شود، مسیر نسبی کامل آنرا در اینجا قید نمائید.
یک نکته:
نیازی به حفظ کردن هیچکدام از موارد فوق نیست. برای مثال در خط فرمان پاورشل، دستور Scaffold را نوشته و پس از یک فاصله، دکمه Tab را فشار دهید. لیست پارامترهای قابل اجرای در این حالت ظاهر خواهند شد. اگر در اینجا برای نمونه Controller انتخاب شود، مجددا با ورود یک فاصله و خط تیره و سپس فشردن دکمه Tab، لیست پارامترهای مجاز و همراه با سوئیچ کنترلر ظاهر میگردند.
MVC Scaffolding و مدیریت روابط بین کلاسها
مثال قسمت قبلی بسیار ساده و شامل یک کلاس بود. اگر آنرا کمی پیچیدهتر کرده و برای مثال روابط one-to-many و many-to-many را اضافه کنیم چطور؟
using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace MvcApplication1.Models { public class Task { public int Id { set; get; } [Required] public string Name { set; get; } [DisplayName("Due Date")] public DateTime? DueDate { set; get; } [ForeignKey("StatusId")] public virtual Status Status { set; get; } // one-to-many public int StatusId { set; get; } [StringLength(450)] public string Description { set; get; } public virtual ICollection<Tag> Tags { set; get; } // many-to-many } public class Tag { public int Id { set; get; } [Required] public string Name { set; get; } public virtual ICollection<Task> Tasks { set; get; } // many-to-many } public class Status { public int Id { set; get; } [Required] public string Name { set; get; } } }
در ادامه دستور تولید کنترلرهای Task، Tag و Status ساخته شده با الگوی مخزن را در خط فرمان پاورشل ویژوال استودیو صادر میکنیم:
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force PM> Scaffold Controller -ModelType Tag -ControllerName TagsController -DbContextType TasksDbContext -Repository -Force PM> Scaffold Controller -ModelType Status -ControllerName StatusController -DbContextType TasksDbContext -Repository -Force
چند نکته:
- با توجه به اینکه مدلها تغییر کردهاند، نیاز است بانک اطلاعاتی متناظر نیز به روز گردد. مطالب مرتبط با آنرا در مباحث Migrations میتوانید مطالعه نمائید.
- View تولیدی رابطه many-to-many را پشتیبانی نمیکند. این مورد را باید دستی اضافه و طراحی کنید: (^ و ^)
- رابطه one-to-many به خوبی با View متناظری دارای یک drop down list تولید خواهد شد. در اینجا لیست تولیدی به صورت خودکار با مقادیر خاصیت Name کلاس Status پر میشود. اگر این نام دقیقا Name نباشد نیاز است توسط ویژگی به نام DisplayColumn که بر روی نام کلاس قرار میگیرد، مشخص کنید از کدام خاصیت باید استفاده شود.
@Html.DropDownListFor(model => model.StatusId, ((IEnumerable<Status>)ViewBag.PossibleStatus).Select(option => new SelectListItem { Text = (option == null ? "None" : option.Name), Value = option.Id.ToString(), Selected = (Model != null) && (option.Id == Model.StatusId) }), "Choose...") @Html.ValidationMessageFor(model => model.StatusId)
تولید آزمونهای واحد به کمک MVC Scaffolding
MVC Scaffolding امکان تولید خودکار کلاسها و متدهای آزمون واحد را نیز دارد. برای این منظور دستور زیر را در خط فرمان پاورشل وارد نمائید:
PM> Scaffold MvcScaffolding.ActionWithUnitTest -Controller TasksController -Action ArchiveTask -ViewModel Task
نکته مهم آن، عدم حذف یا بازنویسی کامل کنترلر یاد شده است. کاری هم که در تولید متد آزمون واحد متناظر انجام میشود، تولید بدنه متد آزمون واحد به همراه تولید کدهای اولیه الگوی Arrange/Act/Assert است. پر کردن جزئیات بیشتر آن با برنامه نویس است.
و یا به صورت خلاصهتر:
PM> Scaffold UnitTest Tasks Delete
کار مقدماتی با MVC Scaffolding و امکانات مهیای در آن همینجا به پایان میرسد. در قسمتهای بعد به سفارشی سازی این مجموعه خواهیم پرداخت.
تبدیلگر تاریخ شمسی برای AutoMapper
public class User { public int Id { set; get; } public string Name { set; get; } public DateTime RegistrationDate { set; get; } }
public class UserViewModel { public int Id { set; get; } public string Name { set; get; } public string RegistrationDate { set; get; } }
تبدیلگر سفارشی تاریخ میلادی به شمسی مخصوص AutoMapper
در ذیل یک تبدیلگر سفارشی مخصوص AutoMapper را با پیاده سازی اینترفیس ITypeConverter آن ملاحظه میکنید:
public class DateTimeToPersianDateTimeConverter : ITypeConverter<DateTime, string> { private readonly string _separator; private readonly bool _includeHourMinute; public DateTimeToPersianDateTimeConverter(string separator = "/", bool includeHourMinute = true) { _separator = separator; _includeHourMinute = includeHourMinute; } public string Convert(ResolutionContext context) { var objDateTime = context.SourceValue; return objDateTime == null ? string.Empty : toShamsiDateTime((DateTime)context.SourceValue); } private string toShamsiDateTime(DateTime info) { var year = info.Year; var month = info.Month; var day = info.Day; var persianCalendar = new PersianCalendar(); var pYear = persianCalendar.GetYear(new DateTime(year, month, day, new GregorianCalendar())); var pMonth = persianCalendar.GetMonth(new DateTime(year, month, day, new GregorianCalendar())); var pDay = persianCalendar.GetDayOfMonth(new DateTime(year, month, day, new GregorianCalendar())); return _includeHourMinute ? string.Format("{0}{1}{2}{1}{3} {4}:{5}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture), info.Hour.ToString("00"), info.Minute.ToString("00")) : string.Format("{0}{1}{2}{1}{3}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture)); } }
ثبت و معرفی تبدیلگرهای سفارشی AutoMapper
پس از تعریف یک تبدیلگر سفارشی AutoMapper، اکنون نیاز است آنرا به AutoMapper معرفی کنیم:
public class TestProfile1 : Profile { protected override void Configure() { // این تنظیم سراسری هست و به تمام خواص زمانی اعمال میشود this.CreateMap<DateTime, string>().ConvertUsing(new DateTimeToPersianDateTimeConverter()); this.CreateMap<User, UserViewModel>(); } public override string ProfileName { get { return this.GetType().Name; } } }
همانطور که مشاهده میکنید در اینجا دو نگاشت تعریف شدهاند. یکی برای تبدیل User به UserViewModel و دیگری، معرفی نحوهی نگاشت DateTime به string، توسط تبدیلگر سفارشی DateTimeToPersianDateTimeConverter است که به کمک متد الحاقی ConvertUsing صورت گرفتهاست.
باید دقت داشت که تنظیمات تبدیلگرهای سفارشی سراسری هستند و در کل برنامه و به تمام پروفایلها اعمال میشوند.
بررسی خروجی تبدیلگر سفارشی تاریخ
اکنون کار استفاده از تنظیمات AutoMapper با ثبت پروفایل تعریف شده آغاز میشود:
Mapper.Initialize(cfg => // In Application_Start() { cfg.AddProfile<TestProfile1>(); });
var dbUser1 = new User { Id = 1, Name = "Test", RegistrationDate = DateTime.Now.AddDays(-10) }; var uiUser = new UserViewModel(); Mapper.Map(source: dbUser1, destination: uiUser);
نوشتن تبدیلگرهای غیر سراسری
همانطور که عنوان شد، معرفی تبدیلگرها به AutoMapper سراسری است و در کل برنامه اعمال میشود. اگر نیاز است فقط برای یک مدل خاص و یک خاصیت خاص آن تبدیلگر نوشته شود، باید نگاشت مورد نظر را به صورت ذیل تعریف کرد:
this.CreateMap<User, UserViewModel>() .ForMember(userViewModel => userViewModel.RegistrationDate, opt => opt.ResolveUsing(src => { var dt = src.RegistrationDate; return dt.ToShortDateString(); }));
خصوصی سازی تبدیلگرها با تدارک موتورهای نگاشت اختصاصی
اگر میخواهید تنظیمات TestProfile1 به کل برنامه اعمال نشود، نیاز است یک MappingEngine جدید و مجزای از MappingEngine سراسری AutoMapper را ایجاد کرد:
var configurationStore = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers); configurationStore.AddProfile<TestProfile1>(); var mapper = new MappingEngine(configurationStore); mapper.Map(source: dbUser1, destination: uiUser);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
AM_Sample02.zip
var app = angular.module('myApp', []);
app.directive('angry', function () { return { restrict: 'E', template: '<div style="color:red"> I am angry!</div>' } })
restrict: که چهار مقدار E و A و C و M را میپذیرد که به EACM نیز معروف هستند.
E: زمانی که قصد داشته باشیم یک المان جدید بسازیم از E به معنای element در restrict استفاده میکنیم(<my-directive></my-directive> )؛
A: زمانی که قصد داشته باشیم Directive مورد نظر به عنوان Attribute در تگها استفاده شود از A به معنای Attribute در restrict استفاده میشود(<div my-directive="exp"></div> )؛
C: از C نیز برای تعریف Directive به عنوان مقادیر ویژگی کلاس استفاده میکنیم(<div class="my-directive: exp "></div> )؛
M: حالت M نیز برای استفاده Directive در کامنتها است(<!-- directive: my-directive exp --> ).
در ادامه یک Directive دیگر به نام happy میسازیم:
app.directive('happy', function () { return { restrict: 'A', template: '<div style="color:blue"> I am happy!</div>' }; })
<script type="text/javascript" src="~/scripts/Modules/module1.js"></script> <div ng-app="myApp"> <angry></angry> <div happy></div> </div>
ادامه دارد...
در این قسمت میتوانید هر کدام از موارد را که نیاز دارید، نصب کنید. هر کدام از عنوانها در آینده آموزش داده خواهند شد و پیشنهاد میشود آنها را نصب کنید؛ در غیر این صورت فقط گزینهی اول کافی میباشد.
در صورت کلیک بر روی Options، با صفحهی زیر روبرو میشوید که در آن با انتخاب گزینهی Administrative، میتوانید تنظیمات بیشتری را در زمان نصب، انجام دهید و همچنین با انتخاب All users ، نرم افزار برای تمام کاربران سیستم در دسترس خواهد بود.
بعد از اتمام نصب، فایلهای نرم افزار در آدرس زیر در دسترس میباشند:
%LOCALAPPDATA%\JetBrains\Installations
اکنون اگر Visual studio را باز کنید، Resharper به قسمت Extensionsها اضافه شدهاست که در ادامهی آموزش به بررسی آن میپردازیم.
زبانهای پشتیبانی شده
ریشارپر از زبان های C# , VB.NET , TypeScript , JavaScript , C++ , CSS پشتیبانی میکند.
افزایش سرعت Reshaper
یکی از مشکلاتی که بیشتر افراد بعد از نصب این افزونه دارند، مخصوصا اگر سیستم آنها خیلی قوی نباشد، کند شدن Visual Studio میباشد. برای سریعتر کردن این افزونه راه هایی موجود میباشد که به آنها میپردازیم.
- خود ریشارپر پیشنهادهایی را برای بهبود سرعت میکند که آنها در مسیر زیر در دسترس میباشند:
ReSharper | Options | Environment | Performance Guide
- یکی از امکانات ریشارپر، نشان دادن تمام خطاهای موجود در برنامه به صورت یک لیست میباشد که نام این ویژگی، solution-wide analysis است و البته این امکان باعث سنگینی زیاد Visual studio میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید:
ReSharper | Options | Code Inspection | Settings
- راه دیگر انجام اینکار از قسمت تنظیمات خود Visual studio است. برای این کار به مسیر زیر بروید و گزینههای گفته شده را غیر فعال کنید.
Environment | General
Automatically adjust visual experience based on client performance Enable rich client visual experience
- همچنین این گزینه را نیز فعال کنید تا جلوی لگ در UI گرفته شود.
Use hardware graphics acceleration if available
- اگر پروژهی بزرگی را دارید، میتوانید گزینهی زیر را نیز غیر فعال کنید. البته با غیر فعال کردن این گزینه در صورت کرش نرم افزار، کدهای ذخیره نشده ازدست میروند.
Environment | AutoRecover
Save AutoRecover information
- اگر با تعداد فایلهای زیادی کار میکنید، امکان Track changes باعث کندی برنامه میشود. برای غیر فعال کردن این گزینه، به مسیر زیر بروید.
Text Editor | General
Track changes
- خود Visual studio گزینههایی را مانند خطاها در Scroll Bar نشان میدهد که این امکانات در ریشارپر هم موجود میباشد. برای غیر فعال کردن این امکان در Visual Studio برای جلوگیری از دو بار نشان دادن اطلاعات، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | All Languages | Scroll Bars
Show annotations over vertical scroll bar
- یکی دیگر از امکانات Visual Studio گزینهای به اسم CodeLens می باشد که یکی از کارهای آن، نشان دادن تمام رفرنسهای توابع یک فایل، در بالای تابع میباشد. این امکان نیز باعث کندی بسیار زیاد برنامه میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید.
Text Editor | All Languages | CodeLens
- هم Visual Studio و هم Reshaper کدهای شما را Format میکنند. پس برای جلوگیری از دوبار انجام شدن این کار، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | [Language] | Formatting
auto-formatting
- اگر از تمام امکانات Reshaper نمیخواهید استفاده کنید، میتوانید آنها را از آدرس زیر غیر فعال کنید.
Environment | Products & Features
- اگر در زمان تایپ کردن، برنامه کند میباشد، میتوانید بعضی از امکانات Resharper را از آدرس زیر غیر فعال کنید.
Environment | IntelliSense
Completion Appearance ReSharper's IntelliSense for specific languages
نمایش منتظر بمانید در حین بارگذاری اولیهی کامپوننت
کامپوننتهایی که قرار است اطلاعات را از یک Web API دریافت کنند، مدتی باید منتظر بمانند تا عملیات رفت و برگشت به سرور، تکمیل شود. در این بین میتوان یک loading را به کاربر نمایش داد:
@page "/hotel/rooms" @if (Rooms is not null && Rooms.Any()) { } else { <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;"> <img src="images/loader.gif" /> </div> } @code { IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>(); // ... }
- هر زمانیکه کار روال رویدادگردان OnInitializedAsync به پایان برسد (که شامل اجرای متد LoadRooms نیز هست)، سبب فراخوانی خودکار StateHasChanged میشود. این فراخوانی، UI را مجددا رندر میکند. به همین جهت است که پس از پایان کار، محتوای if، رندر خواهد شد.
- از این loading سفارشی که در میانهی صفحه نمایش داده میشود، میتوان در فایل wwwroot\index.html نیز بجای loading پیشفرض آن استفاده کرد:
<body> <div id="app"> <div style=" position: fixed; top: 50%; left: 50%; margin-top: -50px; margin-left: -100px; " > <img src="images/ajax-loader.gif" /> </div> </div>
افزودن خواصی جدید به HotelRoomDTO
میخواهیم به کاربر امکان تغییر تعداد روزهای اقامت را بدهیم. این انتخاب باید در لیست اتاقهای نمایش داده شده، با تغییر تعداد روزهای اقامت (TotalDays) و هزینهی جدید متناظر با آن (TotalAmount)، منعکس شود. به همین جهت این خواص را به HotelRoomDTO، اضافه میکنیم:
namespace BlazorServer.Models { public class HotelRoomDTO { // ... public int TotalDays { get; set; } public decimal TotalAmount { get; set; } } }
@code { HomeVM HomeModel = new HomeVM(); // ... private async Task LoadRoomsAsync() { Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate); foreach (var room in Rooms) { room.TotalAmount = room.RegularRate * HomeModel.NoOfNights; room.TotalDays = HomeModel.NoOfNights; } } }
افزودن امکان تغییر تعداد روزهای اقامت در همان صفحهی نمایش لیست اتاقها
همانطور که در تصویر فوق هم مشاهده میکنید، میخواهیم در این صفحه نیز کاربر بتواند زمان شروع اقامت و مدت مدنظر را تغییر دهد. به همین جهت، HomeModel ای را که در قسمت قبل از Local Storage دریافت کردیم، به فرم زیر متصل میکنیم تا اجزای آن در این فرم، نمایش داده شده و قابل تغییر شوند:
@if (Rooms is not null && Rooms.Any()) { <EditForm Model="HomeModel" OnValidSubmit="SaveBookingInfo" class="bg-light"> <div class="pt-3 pb-2 px-5 mx-1 mx-md-0 bg-secondary"> <DataAnnotationsValidator /> <div class="row px-3 mx-3"> <div class="col-6 col-md-4"> <div class="form-group"> <label class="text-warning">Check in Date</label> <InputDate @bind-Value="HomeModel.StartDate" class="form-control" /> </div> </div> <div class="col-6 col-md-4"> <div class="form-group"> <label class="text-warning">Check Out Date</label> <input @bind="HomeModel.EndDate" disabled="disabled" readonly="readonly" type="date" class="form-control" /> </div> </div> <div class=" col-4 col-md-2"> <div class="form-group"> <label class="text-warning">No. of nights</label> <select class="form-control" @bind="HomeModel.NoOfNights"> <option value="Select" selected disabled="disabled">(Select No. Of Nights)</option> @for (var i = 1; i <= 10; i++) { <option value="@i">@i</option> } </select> </div> </div> <div class="col-8 col-md-2"> <div class="form-group" style="margin-top: 1.9rem !important;"> @if (IsProcessing) { <button class="btn btn-success btn-block form-control"> <i class="fa fa-spin fa-spinner"></i>Processing... </button> } else { <input type="submit" value="Update" class="btn btn-success btn-block form-control" /> } </div> </div> </div> </div> </EditForm>
@code { bool IsProcessing; // ... private async Task SaveBookingInfo() { IsProcessing = true; HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights); await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel); await LoadRoomsAsync(); IsProcessing = false; } }
سؤال: زمانیکه IsProcessing به true تنظیم میشود که هنوز کار متد رویدادگردان SaveBookingInfo به پایان نرسیدهاست و فراخوانی خودکار StateHasChanged در پایان متدهای رویدادگردان صورت میگیرد. پس چطور است که سبب رندر مجدد UI و تغییر برچسب دکمهی Update میشود؟
پاسخ به این سؤال را در قسمت 6 این سری با بررسی چرخهی حیات کامپوننتها، مشاهده کردیم:
«البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged میشوند؛ یکبار زمانیکه قسمت sync متد به پایان میرسد (در این مثال یعنی تا قبل از اولین await نوشته شده) و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید»
نمایش لیست اتاقها
نمایش لیست اتاقها مطابق تصویر فوق، دو قسمت اصلی را دارد:
الف) نمایش لیست تصاویر منتسب به یک اتاق، توسط کامپوننت carousel بوت استرپ
@foreach (var room in Rooms) { <div class="row p-2 my-3 " style="border-radius:20px; border: 1px solid gray"> <div class="col-12 col-lg-3 col-md-4"> <div id="carouselExampleIndicators_@room.Id" class="carousel slide mb-4 m-md-3 m-0 pt-3 pt-md-0" data-ride="carousel"> <ol class="carousel-indicators"> @{ int imageIndex = 0; int innerImageIndex = 0; } @foreach (var image in room.HotelRoomImages) { if (imageIndex == 0) { <li data-target="#carouselExampleIndicators_@room.Id" data-slide-to="@imageIndex" class="active"></li> } else { <li data-target="#carouselExampleIndicators_@room.Id" data-slide-to="@imageIndex"></li> } imageIndex++; } </ol> <div class="carousel-inner"> @foreach (var image in room.HotelRoomImages) { var imageUrl = $"{ImagesBaseAddress}/{image.RoomImageUrl}"; if (innerImageIndex == 0) { <div class="carousel-item active"> <img class="d-block w-100" style="border-radius:20px;" src="@imageUrl" alt="First slide"> </div> } else { <div class="carousel-item"> <img class="d-block w-100" style="border-radius:20px;" src="@imageUrl" alt="First slide"> </div> } innerImageIndex++; } </div> <a class="carousel-control-prev" href="#carouselExampleIndicators_@room.Id" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="sr-only">Previous</span> </a> <a class="carousel-control-next" href="#carouselExampleIndicators_@room.Id" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="sr-only">Next</span> </a> </div> </div> }
- سپس در حلقهای که برای نمایش لیست اتاقها تهیه کردهایم، قسمتهای مختلف carousel را تکمیل میکنیم که در اینجا نیاز به ایندکس تصاویر، لیست تصاویر و یک Id منحصربفرد برای این carousel خاص را دارد تا بتوان چندین وهله از آنرا در صفحه قرار داد که این id را بر اساس Id اتاق مشخص کردهایم.
دو نکته:
- در این مثال برای تعریف لینک به تصاویر، کد زیر را مشاهده میکنید:
var imageUrl = $"{ImagesBaseAddress}/{image.RoomImageUrl}";
@code { string ImagesBaseAddress = "https://localhost:5006";
- کامپوننت carousel برای اجرا، نیاز به فایل lib/bootstrap/dist/js/bootstrap.bundle.min.js را نیز دارد. به همین جهت مدخل اسکریپت آنرا باید به فایل wwwroot\index.html اضافه کرد.
ب) نمایش جزئیات نام و هزینهی اتاق
قسمت دوم حلقهی foreach نمایش لیست اتاقها، جهت نمایش جزئیات هر اتاق تعریف شدهاست:
@foreach (var room in Rooms) { <div class="col-12 col-lg-9 col-md-8"> <div class="row pt-3"> <div class="col-12 col-lg-8"> <p class="card-title text-warning" style="font-size:xx-large">@room.Name</p> <p class="card-text"> @((MarkupString)room.Details) </p> </div> <div class="col-12 col-lg-4"> <div class="row pb-3 pt-2"> <div class="col-12 col-lg-11 offset-lg-1"> <a href="@($"hotel/room-details/{room.Id}")" class="btn btn-success btn-block">Book</a> </div> </div> <div class="row "> <div class="col-12 pb-5"> <span class="float-right"> <span class="float-right">Occupancy : @room.Occupancy adults </span><br /> <span class="float-right pt-1">Room Size : @room.SqFt sqft</span><br /> <h4 class="text-warning font-weight-bold pt-4"> <span style="border-bottom:1px solid #ff6a00"> @room.TotalAmount.ToString("#,#.00;(#,#.00#)") </span> </h4> <span class="float-right">Cost for @room.TotalDays nights</span> </span> </div> </div> </div> </div> </div> </div> }
- هر اتاق نمایش داده شده، لینکی را به صفحهی خاص خودش نیز دارد که آنرا در قسمت بعدی تکمیل میکنیم.
- در اینجا TotalAmount و TotalDays محاسباتی و قابل تغییر بر اساس انتخاب کاربر نیز درج شدهاند.
یک تمرین: در برنامهی Blazor Server، سرویسی را جهت درج مشخصات امکانات رفاهی هتل تهیه کردیم. این امکانات رفاهی را از طریق Web API برنامه دریافت و سپس در برنامهی سمت کلاینت نمایش دهید.
بنابراین تکمیل این تمرین شامل تهیهی موارد زیر است که کدنویسی آن، با دو قسمت اخیر این سری دقیقا یکی است و نکتهی جدیدی را به همراه ندارد (و کدهای کامل آن را از انتهای بحث میتوانید دریافت کنید):
- تهیهی HotelAmenityController در پروژهی Web API که به کمک IAmenityService، لیست امکانات رفاهی را بازگشت میدهد.
- تهیهی ClientHotelAmenityService در پروژهی WASM که همانند ClientHotelRoomService قسمت قبل ، از Web API، لیست HotelAmenityDTOها را دریافت میکند.
- ثبت سرویس جدید ClientHotelAmenityService در Program.cs.
- در آخر حلقهای را بر روی لیست HotelAmenityDTO دریافتی از ClientHotelRoomService در کامپوننت Index.razor تشکیل داده و آنها را نمایش میدهیم.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-28.zip