نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت پنجم - پیاده سازی ورود و خروج از سیستم
- همان مسیری را که عنوان کرده (https://localhost:6001/.well-known/openid-configuration) در مرورگر باز کنید؛ باید قابل دسترسی باشد.
- اگر قابل دسترسی بود، بیشتر مشکل HTTPS هست. برای این منظور، حتما نیاز هست با قسمت «تنظیم مجوز امضای توکن‌های IDP» آشنا باشید و آن‌را پیاده سازی کرده باشید (مهم و اجباری!)
- غیرفعال کردن بررسی مجوز در Xamarin این نکات را هم به همراه دارد:
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;

var options = new OidcClientOptions(); 
options.BackchannelHandler = new HttpClientHandler
{ 
  ServerCertificateCustomValidationCallback = (message, certificate, chain, sslPolicyErrors) => true
};
مطالب
قراردادن کلاس های Metdata در اسمبلی جداگانه برای کار با WCF Ria Service توسط FluentMetadata
اصلی‌ترین مزیت این پکیج ،امکان جداکردن dataModel و Metadata در پروژه یا اسمبلی جداگانه است . در حالیکه WCF RIA Service استاندارد فاقد این قابلیت میباشد و باید dataModel و Metadata در یک پروژه و در یک namespace تعریف شوند. 
برای استفاده از FluentMetadata :
1) ابتدا فرض می‌کنیم که کلاس Product ما در یک اسمبلی دیگر به نام DataModel تعریف شده است ، بصورت زیر :
public class Product
{
        public int ProductId { get; set; }
        public string ProductName { get; set; }
        public long ProductPrice { get; set; }
}
2) حال یک پروژه جدید به نام DataModelsMetadata تعریف می‌کنیم و ارجاعی به اسمبلی بالا یعنی DataModel نیز به آن اضافه می‌کنیم .
2-1) ابتدا باید پکیج FluentMetadata را توسط Nuget نصب کرد. راهنمای نصب
2-2) سپس کلاس‌های Metadata موردنظر خود را برای کلاس Product تعریف میکنیم .
public class ProductMetadata : MetadataClass<Product>
{
    public ProductMetadata ()
    {
        this.Validation(x => x.ProductName).Required("عنوان محصول وارد نشده است");
        this.Validation(x => x.ProductPrice).Range(1000,100000,"قیمت محصول باید بین هزار تومان تا صدهزار تومان باشد");
    }
}
2-3) سپس یک کلاس MetadataConfiguration که برای نمونه سازی تمام کلاس‌های متادیتا ایجاد می‌کنیم.
public class FluentMetadataConfiguration : IFluentMetadataConfiguration
{
    public void OnTypeCreation(MetadataContainer metadataContainer)
    {
        metadataContainer.Add(new ProductMetadata());
    }
}
2-4) در آخر اضافه کردن MetadataConfiguration  ایجاد شده به Domain Service توسط ویژگی FluentMetadata.
[EnableClientAccess()]
[FluentMetadata(typeof(FluentMetadataConfiguration))]
public class FluentMetadataTestDomainService : DomainService
{
    ...
}
منابع :
اشتراک‌ها
ایده گرفتن از قسمت‌های خوب Domain Driven Design

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. 

ایده گرفتن از قسمت‌های خوب Domain Driven Design
اشتراک‌ها
مقدمه‌ای بر 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  

مقدمه‌ای بر NET MAUI.
مطالب دوره‌ها
پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
در این قسمت قصد داریم با نحوه پیاده سازی امتیاز دهی ستاره‌ای به مطالب، که نمونه‌ای از آن‌را در سایت جاری در قسمت‌های مختلف آن مشاهده می‌کنید، آشنا شویم.


مدل برنامه

در ابتدای کار نیاز است تا ساختاری را جهت ارائه لیستی از مطالب که دارای گزینه امتیاز دهی می‌باشند، تهیه کنیم:
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; }
    }
}
اگر با EF Code first آشنا باشید، خاصیت Rating تعریف شده در اینجا می‌تواند از نوع ComplexType تعریف شود که شامل جمع امتیازهای داده شده، تعداد کل رای دهنده‌ها و همچنین میانگین امتیازهای حاصل است.


منبع داده فرضی برنامه

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;
}
برای نمایش ستاره‌ها و کار با تصویر Images/star_rating.png (که در پروژه پیوست قرار دارد) ابتدا نیاز است CSS فوق را به پروژه خود اضافه نمائید.

افزودن افزونه 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);
// ]]>
اطلاعات فوق، فایل jquery.StarRating.js را تشکیل می‌دهند که باید به پروژه اضافه گردند.
کاری که این افزونه انجام می‌دهد ردیابی حرکت ماوس بر روی ستاره‌های نمایش داده شده و سپس ارسال سه پارامتر ذیل به اکشن متدی که توسط پارامتر postInfoUrl مشخص می‌گردد، پس از کلیک کاربر می‌باشد:
 { postID: pID, rating: newRating, sectionType: type }
همانطور که ملاحظه می‌کنید به ازای هر قطعه رای گیری که به صفحه اضافه می‌شود، Id مطلب، رای داده شده و نام قسمت جاری، به اکشن متدی خاص ارسال خواهند گردید. sectionType از این جهت اضافه گردیده است تا بتوانید با بیش از یک جدول کار کنید و از این افزونه در قسمت‌های مختلف سایت به سادگی بتوانید استفاده نمائید.
در اینجا از 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>
        }
        &nbsp;
        @if (postRatingsCount > 0)
        {
            var ratingInfo = string.Format(CultureInfo.InvariantCulture, "امتیاز {0:0.00} از 5 توسط {1} نفر", average, postRatingsCount);
            <span>@ratingInfo</span>                
        }
        else
        {
            <span></span>
        }
    </span>
}
از این Html helper برای تشکیل ساختار نمایش قطعه امتیاز دهی به یک مطلب استفاده خواهیم کرد که توسط افزونه جی‌کوئری فوق ردیابی می‌شود.


کنترلر ذخیره سازی اطلاعات دریافتی برنامه

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);
        }
    }
}
در اینجا یک سری متد الحاقی را ملاحظه می‌کنید که برای ثبت اطلاعات رای داده شده یک کاربر بر اساس Id مطلب و نام قسمت متناظر با آن در یک کوکی طراحی شده‌اند. بدیهی است اگر تمام قسمت‌های برنامه شما محافظت شده هستند و کاربران حتما نیاز است ابتدا به سیستم لاگین نمایند، می‌توانید این قسمت را حذف کرده و اطلاعات postId و SectionType را به ازای هر کاربر، جداگانه در بانک اطلاعاتی ثبت و بازیابی نمائید (دقیق‌ترین حالت ممکن؛ البته برای سیستمی بسته که حتما تمام قسمت‌های آن نیاز به اعتبار سنجی دارند).


پیشنهادی در مورد نحوه ذخیره سازی اطلاعات دریافتی

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.
        }
    }
}
همانطور که عنوان شد، سه داده Id مطلب، رای داده شده و نام قسمت متناظر به اکشن متد ارسال می‌شود. از نام قسمت، برای انتخاب سرویس ذخیره سازی اطلاعات استفاده خواهیم کرد. این سرویس می‌تواند شامل متدی به نام SaveRating، همانند کدهای فوق باشد که Id مطلب و عدد رای حاصل به آن ارسال می‌گردند. ابتدا بر اساس این Id، مطلب متناظر یافت شده و سپس اطلاعات Rating آن به روز خواهد شد. در پایان هم ذخیره سازی اطلاعات باید صورت گیرد.



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>
کدهای _ItemsList.cshtml را در اینجا ملاحظه می‌کند که در آن نحوه فراخوانی متد کمکی StarRatingHelper.AddStarRating ذکر شده است.
اگر به کدهای آن دقت کنید از 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>
}
در این View لیستی از مطالب دریافت و به partial view طراحی شده برای نمایش ارسال می‌شود. سپس افزونه StarRating نیز تنظیم و به صفحه اضافه خواهد گردید. نکته مهم آن تعیین صحیح اکشن متدی است که قرار است اطلاعات را دریافت کند و نحوه مقدار دهی آن‌را توسط متغیر postInfoUrl مشاهده می‌کنید.

دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample03.zip
مطالب
MVC Scaffolding #2
از آنجائیکه اصل کار با MVC Scaffolding از طریق خط فرمان پاورشل انجام می‌شود، بنابراین بهتر است در ادامه با گزینه‌ها و سوئیچ‌های مرتبط با آن بیشتر آشنا شویم.
دو نوع پارامتر حین کار با 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 تعریف شده اینبار دارای رابطه many-to-many با برچسب‌های مرتبط با آن است. همچنین یک رابطه one-to-many با کلاس وضعیت هر Task نیز تعریف شده است. به علاوه نکته تعریف «کار با کلیدهای اصلی و خارجی در EF Code first» نیز در اینجا لحاظ گردیده است.
در ادامه دستور تولید کنترلر‌های 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
اگر به کارهایی که در اینجا انجام می‌شود دقت کنیم، می‌توان صرفه جویی زمانی قابل توجهی را شاهد بود؛ خصوصا در برنامه‌هایی که از ده‌ها فرم ورود اطلاعات تشکیل شده‌اند. فرض کنید قصد استفاده از ابزار فوق را نداشته باشیم. باید به ازای هر عملیات CRUD دو متد را ایجاد کنیم. یکی برای نمایش و دیگری برای ثبت. بعد بر روی هر متد کلیک راست کرده و Viewهای متناظری را ایجاد کنیم. سپس مجددا یک سری پیاده سازی «مقدماتی» تکراری را به ازای هر متد جهت ثبت یا ذخیره اطلاعات تدارک ببینیم. اما در اینجا پس از طراحی کلاس‌های برنامه، با یک دستور، حجم قابل توجهی از کدهای «مقدماتی» که بعدها مطابق نیاز ما سفارشی سازی و غنی‌تر خواهند شد، تولید می‌گردند.

چند نکته:
- با توجه به اینکه مدل‌ها تغییر کرده‌اند، نیاز است بانک اطلاعاتی متناظر نیز به روز گردد. مطالب مرتبط با آن‌را در مباحث 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
دستوری که در اینجا صادر شده است نسبت به حالت‌های کلی قبلی، اندکی اختصاصی‌تر است. این دستور بر روی کنترلری به نام TasksController، جهت ایجاد اکشن متدی به نام ArchiveTask با استفاده از کلاس ViewModel ایی به نام Task اجرا می‌شود. حاصل آن ایجاد اکشن متد یاد شده به همراه کلاس TasksControllerTest است؛ البته اگر حین ایجاد پروژه جدید در ابتدای کار، گزینه ایجاد پروژه آزمون‌های واحد را نیز انتخاب کرده باشید. نام پروژه پیش فرضی که جستجوی می‌شود YourMvcProjectName.Test/Tests است.
 نکته مهم آن، عدم حذف یا بازنویسی کامل کنترلر یاد شده است. کاری هم که در تولید متد آزمون واحد متناظر انجام می‌شود، تولید بدنه متد آزمون واحد به همراه تولید کدهای اولیه الگوی Arrange/Act/Assert است. پر کردن جزئیات بیشتر آن با برنامه نویس است.
و یا به صورت خلاصه‌تر:
 PM> Scaffold UnitTest Tasks Delete
در اینجا متد آزمون واحد کنترلر Tasks و اکشن متد Delete آن، تولید می‌شود.

کار مقدماتی با MVC Scaffolding و امکانات مهیای در آن همینجا به پایان می‌رسد. در قسمت‌های بعد به سفارشی سازی این مجموعه خواهیم پرداخت.
مطالب دوره‌ها
تبدیلگر تاریخ شمسی برای AutoMapper
فرض کنید مدل معادل با جدول بانک اطلاعاتی ما چنین ساختاری را دارد:
public class User
{
    public int Id { set; get; }
    public string Name { set; get; }
    public DateTime RegistrationDate { set; get; }
}
و ViewModel ایی که قرار است به کاربر نمایش داده شود این ساختار را دارد:
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
    public string RegistrationDate { set; get; }
}
در اینجا می‌خواهیم حین تبدیل User به UserViewModel، تاریخ میلادی به صورت خودکار، تبدیل به یک رشته‌ی شمسی شود. برای مدیریت یک چنین سناریوهایی توسط AutoMapper، امکان نوشتن تبدیلگرهای سفارشی نیز پیش بینی شده‌است.


تبدیلگر سفارشی تاریخ میلادی به شمسی مخصوص 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));
    } 
}
ITypeConverter دو پارامتر جنریک را قبول می‌کند. پارامتر اول نوع ورودی و پارامتر دوم، نوع خروجی مورد انتظار است. در اینجا باید خروجی متد Convert را بر اساس آرگومان دوم ITypeConverter مشخص کرد. توسط ResolutionContext می‌توان به برای مثال context.SourceValue که معادل DateTime دریافتی است، دسترسی یافت. سپس این DateTime را بر اساس متد toShamsiDateTime تبدیل کرده و بازگشت می‌دهیم.


ثبت و معرفی تبدیلگرهای سفارشی 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; }
    }
}
جهت مدیریت بهتر نگاشت‌های AutoMapper ابتدا یک کلاس Profile را آغاز خواهیم کرد و سپس توسط متدهای CreateMap، کار معرفی نگاشت‌ها را آغاز می‌کنیم.
همانطور که مشاهده می‌کنید در اینجا دو نگاشت تعریف شده‌اند. یکی برای تبدیل User به UserViewModel و دیگری، معرفی نحوه‌ی نگاشت DateTime به string، توسط تبدیلگر سفارشی DateTimeToPersianDateTimeConverter است که به کمک متد الحاقی ConvertUsing صورت گرفته‌است.
باید دقت داشت که تنظیمات تبدیلگرهای سفارشی سراسری هستند و در کل برنامه و به تمام پروفایل‌ها اعمال می‌شوند.


بررسی خروجی تبدیلگر سفارشی تاریخ

اکنون کار استفاده از تنظیمات AutoMapper با ثبت پروفایل تعریف شده آغاز می‌شود:
Mapper.Initialize(cfg => // In Application_Start()
{
     cfg.AddProfile<TestProfile1>();
});
سپس نحوه‌ی استفاده از متد Mapper.Map همانند قبل خواهد بود:
var dbUser1 = new User
{
    Id = 1,
    Name = "Test",
    RegistrationDate = DateTime.Now.AddDays(-10)
};
 
var uiUser = new UserViewModel();

Mapper.Map(source: dbUser1, destination: uiUser);
در اینجا در حین کار تبدیل و نگاشت dbUser به uiUser، زمانیکه AutoMapper به هر خاصیت DateTime ایی می‌رسد، مقدار آن‌را با توجه به تبدیلگر سفارشی تاریخی که به آن معرفی کردیم، تبدیل به معادل رشته‌ای شمسی می‌کند.


نوشتن تبدیلگرهای غیر سراسری

همانطور که عنوان شد، معرفی تبدیلگرها به AutoMapper سراسری است و در کل برنامه اعمال می‌شود. اگر نیاز است فقط برای یک مدل خاص و یک خاصیت خاص آن تبدیلگر نوشته شود، باید نگاشت مورد نظر را به صورت ذیل تعریف کرد:
this.CreateMap<User, UserViewModel>()
             .ForMember(userViewModel => userViewModel.RegistrationDate,
                        opt => opt.ResolveUsing(src =>
                        {
                             var dt = src.RegistrationDate;
                             return dt.ToShortDateString();
                        }));
اینبار در همان کلاس پروفایل ابتدای بحث، نگاشت User به ViewModel آن با کمک متد ForMember، سفارشی سازی شده‌است. در اینجا عنوان شده‌است که اگر به خاصیت ویژه‌ی RegistrationDate رسیدی، مقدار آن‌را با توجه به فرمولی که مشخص شده، محاسبه کرده و بازگشت بده. این تنظیم خصوصی است و به کل برنامه اعمال نمی‌شود.


خصوصی سازی تبدیلگرها با تدارک موتورهای نگاشت اختصاصی

اگر می‌خواهید تنظیمات 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);
به صورت پیش فرض و در پشت صحنه، متد Mapper.Map از یک MappingEngine سراسری استفاده می‌کند. اما می‌توان در یک برنامه چندین MappingEngine مجزا داشت که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample02.zip
مطالب
آشنایی با Directive ها در AngularJs
اگر مطالعه ای اجمالی درباره مزیت‌ها و قدرت‌های فریم ورک Angular داشته باشید یکی از مواردی که بسیار جلب توجه می‌کند مبحث Directive‌ها است. به کمک Directive‌ها در Angular می‌توانید کد‌های HTML خود را توسعه دهید. این توسعه علاوه بر تعریف تگ‌های جدید، شامل توسعه کلاس‌ها و همچنین ویژگی‌های تگ‌های HTML نیز خواهد بود. کد‌های HTML شما بسیار خواناتر و از طرفی با قابلیت استفاده مجدد می‌شود. البته این پست فقط شروع به کار در این مقوله است زیرا مبحث Directive‌ها بسیار گسترده‌تر از آن است که بتوان مطالب آن را در  یک مقاله گنجاند.
برای شروع یک فایل جاوا اسکریپ ایجاد کرده و در ابتدای آن یک ماژول تعریف کنید:
var app = angular.module('myApp', []);
در این جا نام ماژول را myApp انتخاب کردم. حال یک Directive به نام angry (نام دیرکتیو‌ها را با حروف کوچک آغاز کنید )به صورت زیر ایجاد می‌کنیم:
app.directive('angry', function () {
    return {
        restrict: 'E',
        template: '<div style="color:red"> I am angry!</div>'
    }
})
 تابع سازنده Directive مورد نظر که یک آبجکت را برگشت می‌دهد شامل خواص زیر می‌باشد:
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>'
    };
})
تفاوت اصلی بین این دو Directive در نوع restrict آن‌ها می‌باشد. برای استفاده از این Directive‌ها در View می‌توان به صورت زیر عمل نمود:
<script type="text/javascript" src="~/scripts/Modules/module1.js"></script>

<div ng-app="myApp">
    <angry></angry>
    <div happy></div>
</div>
همان طور که می‌بینید دستور Directive اول که را با restrict از نوع E است می‌توان به عنوان یک تگ جدید استفاده کرد. دستور happy به عنوان ویژگی تگ div مورد استفاده قرار می‌گیرد(به دلیل اینکه restrict آن از نوع A است) که در نهایت خروجی ساده مثال بالا به صورت زیر خواهد بود:



ادامه دارد...
مطالب
افزایش سرعت کد نویسی با Resharper - قسمت 01 - معرفی و نصب
در این سری قصد آموزش افزونه‌ی Resharper برای Visual studio را دارم که توسط شرکت Jetbrains برای بهبود امکانات Visual Studio و افزایش سرعت کد نویسی، نوشته شده‌است.

این نرم افزار را از لینک زیر می‌توانید دانلود کنید:

لینک مرجع آموزشی آن هم در زیر در دسترس می‌باشد:
info 

نصب نرم افزار
نصب این افزونه، یکی از راحت‌ترین قسمت‌ها می‌باشد. فقط کافی است از لینک داده شده نرم افزار را دانلود کنید تا پس از اجرا، با صفحه‌ی زیر روبرو شوید:

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

در صورت کلیک بر روی 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

در قسمت‌های بعد به معرفی امکانات Reshaper می‌پردازیم.
مطالب
Blazor 5x - قسمت 28 - برنامه‌ی Blazor WASM - نمایش لیست اطلاعات دریافتی از Web API
در قسمت قبل، سرویس و کامپوننت دریافت اطلاعات اتاق‌ها را از Web API برنامه، تکمیل کردیم. در این قسمت با استفاده از اطلاعات مهیا شده، UI آن‌را نیز تکمیل خواهیم کرد.


نمایش منتظر بمانید در حین بارگذاری اولیه‌ی کامپوننت

کامپوننت‌هایی که قرار است اطلاعات را از یک 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>();
    // ... 
}
- فیلد Rooms را در قسمت قبل، در متد LoadRooms، از Web API دریافت و مقدار دهی کردیم. تا زمان تکمیل عملیات این متد، فیلد Rooms، فاقد عضوی است؛ بنابراین قسمت else شرط فوق اجرا می‌شود که یک loading را نمایش خواهد داد. مابقی UI برنامه در قسمت if آن قرار می‌گیرد.
- هر زمانیکه کار روال رویدادگردان 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; }
    }
}
محاسبات مربوط به این خواص را هم می‌توان در همان کامپوننت HotelRooms.razor، پس از بارگذاری لیست اتاق‌ها از Web API، انجام داد:
@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>
نکته‌ی مهم این فرم، مدیریت قسمت کلیک بر روی دکمه‌ی Update است که سبب فراخوانی روال رویدادگران OnValidSubmit می‌شود:
@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 تنظیم می‌کنیم. این مورد سبب می‌شود تا برچسب دکمه‌ی Update به Processing... تغییر کند. سپس فیلد محاسباتی EndDate را بر اساس اطلاعات جدید فرم، به روز رسانی می‌کنیم. در ادامه، مجددا این اطلاعات را در Local Storage ذخیره سازی کرده و کار LoadRoomsAsync را انجام می‌دهیم که به همراه آن، خواص جدید تعداد روزها و هزینه‌ی اقامت نیز مجددا محاسبه می‌شوند. در آخر برچسب دکمه‌ی Update را به حالت اول باز می‌گردانیم.

سؤال: زمانیکه 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، تهیه شده‌اند.
- سپس در حلقه‌ای که برای نمایش لیست اتاق‌ها تهیه کرده‌ایم، قسمت‌های مختلف carousel را تکمیل می‌کنیم که در اینجا نیاز به ایندکس تصاویر، لیست تصاویر و یک Id منحصربفرد برای این carousel خاص را دارد تا بتوان چندین وهله از آن‌را در صفحه قرار داد که این id را بر اساس Id اتاق مشخص کرد‌ه‌ایم.

دو نکته:
- در این مثال برای تعریف لینک به تصاویر، کد زیر را مشاهده می‌کنید:
var imageUrl = $"{ImagesBaseAddress}/{image.RoomImageUrl}";
و این ImagesBaseAddress، به صورت زیر تعریف شده که همان آدرس برنامه‌ی blazor server ای است که مشخصات اتاق‌ها و تصاویر را ثبت می‌کند:
@code {
   string ImagesBaseAddress = "https://localhost:5006";
بنابراین اگر می‌خواهید تصاویر را هم مشاهده کنید، باید برنامه‌ی مجزای blazor server این سری نیز در حال اجرا باشد.
- کامپوننت 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>
}
- در این مثال از MarkupString استفاده شده تا بتوان یک محتوای HTML ای را در صفحه نمایش داد.
- هر اتاق نمایش داده شده، لینکی را به صفحه‌ی خاص خودش نیز دارد که آن‌را در قسمت بعدی تکمیل می‌کنیم.
- در اینجا 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