نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی
یک نکته‌ی تکمیلی: امکان قرار دادن کامپوننت‌های Blazor در یک اسمبلی دیگر و مشکل مسیریابی
می‌توان کامپوننت‌های مورد نظر را در یک اسمبلی دیگر (بجز برنامه‌ی اصلی) قرار دارد؛ اما در این حالت و پس از افزودن ارجاعی به آن‌ها در فایل csproj برنامه‌ی اصلی، اگر در آن‌ها مسیریابی را تعریف کرده باشید، کار نمی‌کنند. برای رفع این مشکل باید به فایل app.razor مراجعه کرد و سپس این اسمبلی‌های ثالث را به سیستم مسیریابی معرفی نمود:
<Router
    AppAssembly="@typeof(Program).Assembly"
    AdditionalAssemblies="new[] { typeof(Component1).Assembly }">
    @* ... Router component elements ... *@
</Router>
در اینجا ویژگی AdditionalAssemblies، آرایه‌ای از اسمبلی‌های ثالث به همراه کامپوننت‌های Blazor را می‌پذیرد.
مطالب
اعمال SEO بر روی AngularJS
در این بخش قصد داریم سئو را بر روی یک برنامه‌ی نوشته شده با آنگلولار و Asp.net Mvc اعمال نماییم. انگولار جی‌اس، صفحات را با  استفاده از جاوااسکریپت رندر میکند، ولی اکثر کرالر‌ها نمیتوانند جاوااسکریپت را اجرا کنند و موقع اجرای صفحات سایت ما  فقط یک div خالی را میبینند.
کاری که سرویس Prerender یا فیلتر سفارشی AjaxCrawlable برای ما انجام میدهد، درخواست‌هایی را که از طرف کرالرها آمده‌است را شناسایی میکند و مانند یک مرورگر، با استفاده از phantomjs آنرا اجرا میکند و نتیجه‌ی کامل صفحات ما را به صورت اچ تی ام ال استاتیک برمی‌گرداند.
فانتوم جی اس، موتور اختصاصی برای شبیه سازی مرورگر مبتنی بر Webkit می‌باشد. فانتوم جی اس را میتوانید بر روی ویندوز، لینوکس و مک نصب نمایید. فانتوم جی اس یک Console در اختیار برنامه نویس قرار می‌دهد که می‌توان توسط آن، برنامه‌های جاوااسکریپت را اجرا نمود. همچنین فانتوم جی اس میتواند اسکرین شاتی را نیز از محتوای وب سایت ما فراهم نماید.
 برای اینکه صفحات انگولار جی اس،ایندکس شوند سه مرحله وجود دارند:
1- به کرالر اطلاع دهیم که رندر کردن سایت، توسط جاوااسکریپت انجام میگردد؛ با اضافه کردن متاتگ زیر در اچ تی ام ال سایت (البته در حالت استفاده HTML5 push state ) :
<meta name="fragment" content="!">
<base href="/">
2- بعد از اضافه کردن متاتگ بالا، کرالر درخواست‌های خود را به صورت زیر به سایت ما ارسال میکند:
http://www.example.com/?_escaped_fragment_=
ما در این مثال از  HTML5 push state  استفاده میکنیم. بنابراین لینکی مانند http://www.example.com/user/123 توسط کرالر به صورت زیر دیده میشود: 
http://www.example.com/user/123?_escaped_fragment_=
3- اچ تی ام ال کاملا رندر شده توسط سایت ما به کرالر ارسال گردد.
برای رندر کردن  اچ تی ام ال صفحات، چندین روش وجود دارد:
روش اول: میتوانیم از سرویس‌های آماده‌ای همچون Prerender.io   استفاده کنیم که سرویسهایی را برای زبانهای مختلف ارائه کرده‌اند. باتوجه به توضیحات نمونه استفاده از آن در Asp.Net Mvc کافیست در سایت Prerender.io  ثبت نام کرده، Token را دریافت کنیم و در کانفیگ برنامه قرار دهیم و در کلاس PreStart قطعه کد زیر را قرار دهیم:
DynamicModuleUtility.RegisterModule(typeof(Prerender.io.PrerenderModule));
مثال استفاده از Prerender.io را میتوانید از این آدرس Simple_Demo_Prerender.zip دانلود نمایید.
 
یکی از ابزارهای مناسب تست کردن اینکه صفحات توسط کرالر ایندکس میشوند یا خیر، برنامه screamingfrog میباشد.
در پنل Ajax آن، صفحات ایندکس شده ما نمایش داده میشوند. لینکی مشابه زیر را در مرورگر اجرا کرده، با ViewPage Source کردن آن میتوانید نتیجه اچ تی ام ال کاملا رندر شده را مشاهده نمایید.
http://www.example.com/user/123?_escaped_fragment_=
نسخه رایگان سرویس Prerender.io تا 250 صفحه را پوشش میدهد.

روش دوم: فیلتر سفارشی AjaxCrawlable. در اولین قدم نیاز به نصب فانتوم جی اس داریم:
<package id="PhantomJS" version="1.9.2" targetFramework="net452" />
<package id="phantomjs.exe" version="1.9.2.1" targetFramework="net452" />
فایل phantomjs.exe را از پوشه packages\PhantomJS.1.9.2\tools\phantomjs\phantomjs.exe یافته و در پوشه bin برنامه قرار دهید. با Attribute زیر هر درخواستی که توسط کرالر ارسال گردد به اکشن returnHTML منتقل میگردد.
برای اینکه خطای معروف A potentially dangerous Request.Form value was detected from the client را دریافت نکنیم، کافیست قسمتهایی از آدرس را که شامل کاراکترهای خاصی مانند :// میباشند، از url حذف کنیم و در اکشن returnHtml قسمتهای حذف شده را  به url  اضافه نماییم.
کرالرها  با مشاهده تگ fragment، تمام لینکها را به همراه کوئری استرینگ _escaped_fragment_  میفرستند، که ما در سرور باید آنرا  با رشته خالی جایگزین نماییم.
 public class AjaxCrawlableAttribute : System.Web.Mvc.ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;
            var url = request.Url.ToString();
            if (request.QueryString[Fragment] != null && !url.Contains("HtmlSnapshot/returnHTML"))
            {
                url = url.Replace("?_escaped_fragment_=", string.Empty).Replace(request.Url.Scheme + "://", string.Empty);
                url = url.Split(':')[1];
                filterContext.Result = new RedirectToRouteResult(
                   new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
Route‌های پیشفرض را با کدهای زیر جایگزین میکنیم:
public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
             name: "HtmlSnapshot",
             url: "HtmlSnapshot/returnHTML/{*url}",
             defaults: new { controller = "HtmlSnapshot", action = "returnHTML", url = UrlParameter.Optional });

            routes.MapRoute(
            name: "SPA",
            url: "{*catchall}",
            defaults: new { controller = "Home", action = "Index" })
        }
 اضافه کردن این فیلتر به فیلترهای Asp.net Mvc 
 public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
ایجاد کنترلر HtmlSnapshot و متد returnHTML :
Url را به عنوان آرگومان به تابع page.open فایل جاوااسکریپتی فانتوم میدهیم و بعد از اجرای کامل، خروجی را درViewData قرار میدهیم 
public ActionResult returnHTML(string url)
        {
            var prefix = HttpContext.Request.Url.Scheme + "://" + HttpContext.Request.Url.Host+":";
            url = prefix+url;
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
            var startInfo = new ProcessStartInfo
            {
                Arguments = string.Format("{0} {1}", Path.Combine(appRoot, "Scripts\\seo.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output1 = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output1.Replace("<!-- ngView:  -->", "").Replace("ng-view=\"\"", "");
            return View();
        }
در فایل renderHtml.cshtml
@{ 
    Layout = null;
}
@Html.Raw(ViewBag.result)
ایجاد فایل seo.js  در پوشه Scripts سایت :
در این بخش webpage  را ایجاد میکنیم و آدرس صفحه را از[system.args[1  دریافت کرده و عملیات کپچر کردن را آغاز میکنیم و بعد از تکمیل اطلاعات در سرور، کد زیر اجرا میشود:
console.log(page.content)

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();;
page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () {

});

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        console.log(page.content);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);
صفحه Layout.Cshtml
<!DOCTYPE html>
<html ng-app="appOne">
<head>
    <meta name="fragment" content="!">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    <base href="/">
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
    <script src="~/Scripts/angular/angular.js"></script>
    <script src="~/Scripts/angular/angular-route.js"></script>
    <script src="~/Scripts/angular/angular-animate.js"></script>
    <script>
        angular.module('appOne', ['ngRoute'], function ($routeProvider, $locationProvider) {
            $routeProvider.when('/one', {
                template: "<div>one</div>", controller: function ($scope) {
                }
            })
            .when('/two', {
                template: "<div>two</div>", controller: function ($scope) {
                }
            }).when('/', {
                template: "<div>home</div>", controller: function ($scope) {
                }
            });
            $locationProvider.html5Mode({
                enabled: true
            });
        });
    </script>
</head>
<body>
    <div id="body">
        <section ng-view></section>
        @RenderBody()
    </div>
    <div id="footer">
        <ul class='xoxo blogroll'>
            <li><a href="one">one</a></li>
            <li><a href="two">two</a></li>
        </ul>
    </div>
</body>
</html>

چند نکته تکمیلی:
* فانتوم جی اس قادر به اجرای لینکهای فارسی (utf-8) نمیباشد.
 * اگر خطای syntax error را دریافت کردید ممکن است پروژه شما در مسیری طولانی در روی هارد دیسک قرار داشته باشد.
اشتراک‌ها
ساخت یک بازی آنلاین با Blazor و دات نت 7

Spawn an Online Game with Blazor, .NET 7 and Clean Architecture in under 60 minutes

In this fun talk, Luke Parker will show you step-by-step how to build a playable game in under 60 minutes. He will go over some basic game design, project planning, then build the game and finally… play live with the audience! Utilizing code sharing and the magic of Clean Architecture allows for very rapid app development. Also, leveraging MudBlazor as the component library lets you save time and to focus on what really matters.
 

ساخت یک بازی آنلاین با Blazor و دات نت 7
مطالب
نحوه استفاده از ViewModel در ASP.NET MVC

یک Model چیست؟

·   قسمتی از Application است که Domain Logic را پیاده سازی می‌کند.

·   همچنین با عنوان Business Logic نیز شناخته می‌شود.

·   Domain Logic داده‌هایی را که بین UI و دیتابیس پاس داده می‌شود، مدیریت می‌کند.

·   برای مثال، در یک سیستم انبار،Model   کارش ذخیره سازی اقلام در حافظه و Logic تعیین کننده موجود بودن یک آیتم در انبار میباشد.


یک ViewModel چیست؟

·   ViewModel به ما این امکان را میدهد تا از چندین Entity، یک شیء واحد بسازیم.

·   بهینه شده برای استفاده و رندر توسط یک View

 

چرا باید از ViewModel استفاده کنیم؟

  • اگر شما بخواهید چندین Model را به یک View پاس دهید، استفاده از ViewModel ایده بسیار خوبی است.
  • همچنین امکان Validate کردن ViewModel را به روشی متفاوت‌تر از Domain Model برای سناریوهای اعتبارسنجی attribute-based را در اختیارمان قرار میدهد.
  • برای قالبندی داده‌ها نیز استفاده میشود.
        o برای مثال به یک داده یا مقدار پولی قالبندی شده به یک روش خاص نیاز داریم؟
ViewModel بهترین مکان برای انجام این کار است.
  • استفاده از ViewModel تعامل بین Model و View را ساده‌تر می‌کند.  


مکان قرارگیری ViewModel در پروژه بهتر است کجا باشد؟

  1. می‌تواند در یک پوشه با نام ViewModel در ریشه پروژه باشد.

  2. به صورت یک dll ارجاع داده شده به پروژه. 
  3. در یک پروژه جدا به عنوان یک Service Layer.


Best Practice هایی در زمان استفاده از ViewModelها 

  1. داده‌هایی را که قرار است در View رندر شوند، داخل ViewModel قرار دهید.
  2. View باید خصوصیاتی را که نیاز دارد، مستقیما از ViewModel بدست بیاورد.
  3. وقتی که ViewModel شما پیچیده‌تر می‌شود، بهتر است از یک Mapper استفاده کنید.

اجازه دهید مثالی را در این رابطه براتون بیارم :

در یک View می‌خواهیم هم لیست اخبار سایت و هم لیست سخنرانان سایت (مثال مربوط به یک پروژه برای همایش است) را نمایش دهیم. خوب طبق مطالب فوق استفاده از ViewModel بهترین راه حل است. ViewModel مربوطه به صورت زیر تعریف شده است :

public class SpeakerAndNewsViewModel
    {
        public IEnumerable<News> News { get; set; }
        public IEnumerable<Speaker> Speakers { get; set; }
    }
و در اکشن متد Index هم بدین صورت Model‌ها را به ViewModel ساخته شده پاس می‌دهیم :
public ActionResult Index()
        {
            var news = db.News.OrderBy(r=>r.Date)
                        .Take(3);
            var speakers = db.Speakers.Take(4);
            var model = new SpeakerAndNewsViewModel { 
                News=news,
                Speakers=speakers
            };
            return View(model);
        }
و یک View را به صورت Strongly Typed از نوع ViewModel خود که در اینجا SpeakerAndNewsViewModel  است ایجاد می‌کنیم :

و View مربوطه را به شکل زیر تغییر می‌دهیم :  
@model  Project.Models.SpeakerAndNewsViewModel
@{
    ViewBag.Title = "Home Page";
}
@section news
{
@foreach (var item in Model.News)
{
    //..
}
}
@section speakers
{
@foreach (var item in Model.Speakers)
{
    //...
}
}
مطالب
کامپوننت‌های متداول طرحبندی صفحات در بوت استرپ 4
بوت استرپ، به همراه کامپوننت‌هایی برای پیاده سازی اعمال متداول طرحبندی صفحات است؛ مانند jumbotron ،media ،table و card.


کامپوننت jumbotron

از Jumbotron برای نمایش متنی مشخص در بالای یک صفحه، استفاده می‌شود. دو روش استفاده‌ی از آن در بوت استرپ 4 وجود دارند:
- داخل container:
    <div class="container">
        <header class="jumbotron mt-4">
            <div class="display-2 mb-4">Our Mission</div>
            <p class="lead">Wisdom Pet Medicine strives to blend the best in
                traditional and alternative medicine in the diagnosis and
                treatment of companion animals including dogs, cats, birds,
                reptiles, rodents, and fish. We apply the wisdom garnered in
                the centuries old tradition of veterinary medicine, to find the
                safest treatments and cures.</p>
        </header>
با این خروجی:


در اینجا با اعمال کلاس jumbotron، متن header، داخل یک قاب با گوشه‌های گرد قرار می‌گیرد و مشخص‌تر نمایش داده خواهد شد. همچنین با mt-4، فاصله‌ای را بین آن و بالای صفحه ایجاد کرده‌ایم.

- خارج از container:
    <header class="jumbotron jumbotron-fluid">
        <div class="container">
            <div class="display-2 mb-4">Our Mission</div>
            <p class="lead">Wisdom Pet Medicine strives to blend the best in
                traditional and alternative medicine in the diagnosis and
                treatment of companion animals including dogs, cats, birds,
                reptiles, rodents, and fish. We apply the wisdom garnered in
                the centuries old tradition of veterinary medicine, to find the
                safest treatments and cures.</p>
        </div>
    </header>
با این خروجی:


اگر می‌خواهیم این قاب، تمام عرض صفحه را پر کند و همچمنین لبه‌های گرد آن نیز حذف شوند، می‌توان از کلاس jumbotron-fluid استفاده کرد و آن‌را خارج از container قرار داد. سپس برای اینکه متن داخل آن با container زیر آن تراز شود، می‌توان یک container را در اینجا داخل jumbotron تعریف کرد.


کنترل ظاهر جداول، در بوت استرپ 4

بوت استرپ 4 به همراه تعدادی کلاس ویژه است که برای بهبود ظاهر المان استاندارد جدول، ارائه شده‌اند. آن‌ها را در طی مثال‌هایی بررسی خواهیم کرد.


برای رسیدن به چنین تصویری، تغییرات زیر را بر روی یک جدول استاندارد HTML اعمال کرده‌ایم:
<table class="table table-striped table-hover table-bordered table-responsive">
  <thead class="thead-light">
- کلاس table، کلاس پایه اعمال شیوه‌نامه‌های بوت استرپ 4 به المان جدول است که سبب خواهد شد آیتم‌های آن با فاصله‌ی بهتری نسبت به یکدیگر ظاهر شوند. با استفاده از کلاس table-dark می‌توان یک قالب مشکی را به جدول اعمال کرد.
- کلاس table-striped سبب می‌شود تا ردیف‌ها، یک در میان با رنگی متمایز نمایش داده شوند.
- با افزودن table-hover، رنگ ردیف‌های جدول با عبور اشاره‌گر ماوس از روی آن‌ها تغییر می‌کند.
- کلاس table-bordered کار نمایش قاب جدول را انجام می‌دهد.
- کلاس table-responsive سبب می‌شود تا در اندازه‌های کوچک صفحه، یک اسکرول بار افقی برای نمایش آیتم‌های جدول ظاهر شود و یا می‌توان از کلاس table-sm نیز استفاده کرد تا padding تعریف شده‌ی در جدول، کاهش یابند. این کلاس، قابلیت پذیرش break-pointها را نیز دارد؛ مانند table-responsive-md.
- کلاس‌های thead-light و یا thead-dark که بر روی تگ thead قرار می‌گیرند، رنگ پس زمینه‌ی هدر جدول را مشخص می‌کنند.
- برای تغییر رنگ پس زمینه و متن یک ردیف می‌توان از کلاس‌های bg-color و text-color استفاده کرد:
<tr class="bg-danger text-light">
- برای تغییر رنگ سلول‌های جدول از کلاس‌های table-color استفاده می‌کنیم:
<td class="table-success">$100.00 </td>
فرمول‌های رنگ‌های قابل اعمال به ردیف‌ها، سلول‌ها و متون جداول بوت استرپ 4 را در تصویر ذیل مشاهده می‌کنید:



کامپوننت جدید card در بوت استرپ 4

پنل‌های بوت استرپ 3 حذف و بجای آن کامپوننت جدیدی به نام card در نگارش 4 آن ارائه شده‌است که با افزودن کلاس آن به یک div، بلافاصله قابی با گوشه‌های گرد به آن اضافه می‌شود.


        <section class="card mb-5" id="drwinthrop">
            <div class="card-body">
                <img class="card-img img-fluid" src="images/testimonial-mcphersons.jpg"
                    alt="Doctor Winthrop Photo">
                <h2 class="card-title">Dr. Stanley Winthrop</h2>
                <h5 class="card-subtitle">Behaviorist</h5>
                <p class="card-text">Dr. Winthrop is the guardian of Missy, a
                    three-year old Llaso mix, who he adopted at the shelter.
                    Dr. Winthrop is passionate about spay and neuter and pet
                    adoption, and works tireless hours outside the clinic,
                    performing free spay and neuter surgeries for the shelter.</p>
                <a class="card-link" href="#">About Me</a>
                <a class="card-link" href="#">My Pets</a>
                <a class="card-link" href="#">Client Slideshow</a>
            </div>
        </section>
- برای اینکه عناصر داخل card با فاصله‌ی مناسبی از لبه‌های آن قرار گیرند و همچنین شیوه‌نامه‌های قسمت‌های مختلف آن به درستی اعمال شوند، نیاز است محتوای section ای که با کلاس card مشخص شده (تعیین container)، داخل یک div با کلاس card-body قرار گیرد. در اینجا امکان تعریف card-header و card-footer نیز وجود دارد.
- سپس یک card می‌تواند دارای تصویری واکنشگرا باشد که عرض card را پوشش می‌دهد. این تصویر با کلاس card-img مشخص می‌شود.
در اینجا امکان تعریف card-img-top و card-img-bottom نیز وجود دارند. این موارد تصویر card را در بالا و یا پایین آن، بدون padding، نمایش می‌دهند. اگر می‌خواهید متنی را بر روی این تصویر نمایش دهید، از کلاس card-img-overlay استفاده کنید. در این حالت‌ها باید تصویر را خارج از card-body قرار دهید.
- عنوان و زیرعنوان یک card، توسط کلاس‌های card-title و card-subtitle تعیین می‌شوند.
- متن داخل آن‌را با کلاس card-text مشخص می‌کنیم.
- لینک‌های ذیل آن نیز توسط کلاس card-link در طی یک ردیف نمایش داده می‌شوند.

امکان تعیین رنگ پس زمینه، حاشیه و متن یک card نیز وجود دارند:
<section class="card mb-5 bg-primary text-light border-warning" id="drchase">
با این خروجی:


و فرمول کلی رنگ‌های آن نیز به صورت زیر می‌باشد:


می‌توان برای یک card، هدر و فوتر نیز تعریف کرد:
        <section class="card mb-5" id="drsanders">
            <div class="card-header">
                <h2 class="card-title">Dr. Kenneth Sanders</h2>
                <h5 class="card-subtitle">Nutritionist</h5>
            </div>
            <div class="card-body">
                <img class="card-img img-fluid" src="images/testimonial-mcphersons.jpg"
                    alt="Doctor Sanders Photo">
                <p class="card-text">Leroy walked into Dr. Sanders front door
                    when she was moving into a new house. After searching for
                    weeks for Leroy's guardians, she decided to make Leroy a
                    part of her pet family, and now has three cats.</p>
            </div>
            <div class="card-footer">
                <a class="card-link" href="#">About Me</a>
                <a class="card-link" href="#">My Pets</a>
                <a class="card-link" href="#">Client Slideshow</a>
            </div>
        </section>
در اینجا همان card قبلی را مشاهده می‌کنید که عناوین آن به card-header و لینک‌های ذیل آن به card-footer منتقل شده‌اند:


برای تعریف یک list-group در داخل یک card، به صورت زیر عمل می‌کنیم:
        <section class="card mb-5" id="drwong">
            <div class="card-body">
                <img class="card-img img-fluid" src="images/testimonial-mcphersons.jpg"
                    alt="Doctor Wong Photo">
                <h2 class="card-title">Dr. Olivia Wong</h2>
                <h5 class="card-subtitle">Preventive Care</h5>
                <p class="card-text">Dr. Wong is a cancer survivor who was
                    fortunate enough to get to spend time with a therapy dog
                    during her recovery. She became passionate about therapy
                    animals, and has started her own foundation to train and
                    provide education to patients in recovery. Now she gets her
                    own dose of daily therapy from her husky, Lilla.</p>
            </div>
            <div class="list-group list-group-flush">
                <a class="list-group-item" href="#">About Me</a>
                <a class="list-group-item" href="#">My Pets</a>
                <a class="list-group-item" href="#">
                    Client Slideshow
                </a>
            </div>
        </section>
ابتدا list-group را به خارج از card-body منتقل می‌کنیم. سپس برای حذف حاشیه‌ی آن و همچنین گوشه‌های گرد آن، جهت یکی شدن با قاب card، کلاس list-group-flush را به آن اضافه می‌کنیم:



تعیین نحوه‌ی چیدمان cards در بوت استرپ 4

اگر چندین card در یک صفحه تعریف شده‌اند، برای تعیین نحوه‌ی قرارگیری آن‌ها در کنار یکدیگر می‌توان یا از سیستم طرحبندی متداول بوت استرپ استفاده کرده و یا امکان تعریف گروهی از آن‌ها نیز وجود دارد. برای اینکار کافی است یک div با کلاس card-group را تعریف و سپس تمام cards را داخل آن قرار دهیم:
    <div class="container">
        <div class="card-group">
که سبب خواهد شد تمام cards در کنار یکدیگر بدون فاصله‌ای نمایش داده شوند. اگر بجای آن از کلاس card-deck استفاده شود، فاصله‌ای بین cards قرار می‌گیرد.


اگر از کلاس card-columns استفاده کنیم، تمام cards را به صورت خودکار در ستون‌ها و ردیف‌ها، قرار می‌دهد که بعضی از آن‌ها بلندتر و بعضی دیگر کوتاه‌تر هستند (نوعی نمایش کاشی‌کاری شده‌است):


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


المان media در بوت استرپ 4

برای نمایش متداول متن و تصویر که قرار است تصویر، در یک ستون و متن، در ستونی دیگر باشد، بوت استرپ 4 به همراه کلاس media است که بر اساس Flexbox بازنویسی شده‌است.
<body>
    <div class="container">
        <section class="media mb-5" id="drwinthrop">
            <img class="d-flex align-self-center img-fluid rounded mr-3" style="width:30%"
                src="images/testimonial-mcphersons.jpg" alt="Doctor Winthrop Photo">
            <div class="media-body">
                <h2>Dr. Stanley Winthrop</h2>
                <h5>Behaviorist</h5>
                <p>Dr. Winthrop is the guardian of Missy,
                    a three-year old Llaso mix, who he adopted at the
                    shelter. Dr. Winthrop is passionate about spay and neuter
                    and pet adoption, and works tireless hours outside the
                    clinic, performing free spay and neuter surgeries for the
                    shelter.</p>
        </section>
    </div>
</body>
با این خروجی:


ابتدا توسط کلاس media یک container را تعریف می‌کنیم. سپس تصویر، یک ستون و media-body ستون دیگر را تشکیل می‌دهد.
با استفاده از d-flex، المان تصویر را به یک Flexbox container تبدیل کرده و با استفاده از کلاس align-self-center، آن‌را در میانه‌ی ستون قرار می‌دهیم. همچنین در اینجا توسط mr-3، فاصله‌ی آن‌را با متن ستون کناری تنظیم کرده‌ایم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: Bootstrap4_09.zip
نظرات مطالب
Blazor 5x - قسمت دوم - بررسی ساختار اولیه‌ی پروژه‌های Blazor
یک نکته‌ی تکمیلی: بررسی ساختار Layout در برنامه‌های Blazor

بعضی از قسمت‌های صفحه مانند هدر، منوی راهبری، فوتر امثال آن، عموما در تمام صفحات سایت، به یک شکل نمایش داده می‌شوند. جهت کاهش اینگونه کدهای تکراری، در برنامه‌ی ASP.NET Web Forms، مفهوم  Master Page و در برنامه‌های MVC، مفهوم layout page ارائه شد که قسمت‌های مشترک UI را در آن قرار می‌دهند تا دیگر نیازی نباشد به ازای هر صفحه، آن‌ها را تکرار کرد. Layout در برنامه‌های Blazor نیز چنین عملکردی را دارد. از لحاظ فنی، Layout نیز یک کامپوننت Blazor محسوب می‌شود. برای مثال فایل پیش‌فرض Shared\MainLayout.razor با یک چنین ساختاری:
@inherits LayoutComponentBase
<div class="sidebar">
    <NavMenu />
</div>
<div class="main">
    <div class="content px-4">
        @Body
    </div>
</div>
- ابتدا از کلاس LayoutComponentBase ارث‌بری می‌کند که به این ترتیب امکان دسترسی به خاصیت Body را در این کامپوننت میسر خواهد کرد.
- سپس با استفاده از Body@، سبب درج محتوای کامپوننت در حال رندر، در صفحه می‌شود.

DefaultLayout تعریف شده، در فایل آغازین App.razor به تمام کامپوننت‌های برنامه اعمال می‌شود. اما اگر نیاز باشد کامپوننت خاصی از layout دیگری استفاده کند، می‌توان از دایرکتیو layout برای بازنویسی آن، استفاده کرد:
@page "/episodes"
@layout DoctorWhoLayout

<h1>Component 2</h1>
نکته 1: این کامپوننت حتما باید به همراه دایرکتیو page@ نیز باشد؛ یعنی حتما باید routable باشد.
نکته 2: اگر برای کامپوننت‌های خود فایل code-behind تهیه می‌کنید، دایرکتیو layout@ به ویژگی Layout قرار گرفته‌ی بر روی کلاس کامپوننت ترجمه می‌شود:
[Layout(typeof(MainLayout))]

حتی می‌توان یک layout خاص را به پوشه‌ای از کامپوننت‌ها اعمال کرد. برای این منظور در ریشه‌ی این پوشه، فایل Imports.razor_ را قرار داده و سپس دایرکتیو layout@ را به آن اضافه کنید. فایل ویژه‌ی Imports.razor_ را می‌توان به هر پوشه‌ای از کامپوننت‌های برنامه اضافه کرد.

تذکر! دایرکتیو layout@ را در بالاترین فایل Imports.razor_ که در ریشه‌ی پروژه قرار دارد، درج نکنید؛ چون سبب بروز یک حلقه‌ی بی‌نهایت خواهد شد. همانطور که عنوان شد، بالاترین سطح تعریف layout پیش‌فرض، در فایل App.razor انجام می‌شود.
مطالب
قابلیت چند زبانه و Localization در AngularJs- بخش سوم: Best Practiceهای angular-translate
در این بخش قصد دارم تا در قالب یک پروژه، تمامی قابلیت‌هایی را که در angular-translate و ماژول‌های مرتبط با آن وجود دارند، به شما معرفی کنم. پروژه‌ی نمونه را از لینک زیر دریافت نمایید:
AngularJs-Translate-BestPractices.zip 

این پروژه در 12 بخش گوناگون تقسیم بندی شده‌است که هر کدام در قالب یک فایل HTML می‌باشد و تمامی اسکریپت‌های مورد نیاز به آن افزوده شده‌است. هر بخش به صورت مجزا به شرح یک ویژگی کاربردی در angular-translate می‌پردازد.

ex1_basic_usage 

روند کار مثال اول خیلی ساده است. در ابتدا اسکریپت‌های زیر به صفحه اضافه شده‌اند:
    <script src="Scripts/angular.js"></script>
    <script src="Scripts/angular-translate.js"></script>
در ابتدا وابستگی pascalprecht.translate به ماژول اضافه شده‌است و پس از آن زبان‌های مختلف به صورت JSON در بخش اسکریپت وارد شده‌اند.
        angular.module('app', ['pascalprecht.translate'])
        .config([
        '$translateProvider', function ($translateProvider) {

            // Adding a translation table for the English language
            $translateProvider.translations('en_US', {
                "TITLE": "How to use",
                "HEADER": "You can translate texts by using a filter.",
                "SUBHEADER": "And if you don't like filters, you can use a directive.",
                "HTML_KEYS": "If you don't like an empty elements, you can write a key for the translation as an inner HTML of the directive.",
                "DATA_TO_FILTER": "Your translations might also contain any static ({{staticValue}}) or random ({{randomValue}}) values, which are taken directly from the model.",
                "DATA_TO_DIRECTIVE": "And it's no matter if you use filter or directive: static is still {{staticValue}} and random is still {{randomValue}}.",
                "RAW_TO_FILTER": "In case you want to pass a {{type}} data to the filter, you have only to pass it as a filter parameter.",
                "RAW_TO_DIRECTIVE": "This trick also works for {{type}} with a small mods.",
                "SERVICE": "Of course, you can translate your strings directly in the js code by using a $translate service.",
                "SERVICE_PARAMS": "And you are still able to pass params to the texts. Static = {{staticValue}}, random = {{randomValue}}."
            });

            // Adding a translation table for the Russian language
            $translateProvider.translations('ru_RU', {
                "TITLE": "Как пользоваться",
                "HEADER": "Вы можете переводить тексты при помощи фильтра.",
                "SUBHEADER": "А если Вам не нравятся фильтры, Вы можете воспользоваться директивой.",
                "HTML_KEYS": "Если вам не нравятся пустые элементы, Вы можете записать ключ для перевода в как внутренний HTML директивы.",
                "DATA_TO_FILTER": "Ваши переводы также могут содержать любые статичные ({{staticValue}}) или случайные ({{randomValue}}) значения, которые берутся прямо из модели.",
                "DATA_TO_DIRECTIVE": "И совершенно не важно используете ли Вы фильтр или директиву: статическое значение по прежнему {{staticValue}} и случайное - {{randomValue}}.",
                "RAW_TO_FILTER": "Если вы хотите передать \"сырые\" ({{type}}) данные фильтру, Вам всего лишь нужно передать их фильтру в качестве параметров.",
                "RAW_TO_DIRECTIVE": "Это также работает и для директив ({{type}}) с небольшими модификациями.",
                "SERVICE": "Конечно, Вы можете переводить ваши строки прямо в js коде при помощи сервиса $translate.",
                "SERVICE_PARAMS": "И вы все еще можете передавать параметры в тексты. Статическое значение = {{staticValue}}, случайное = {{randomValue}}."
            });

            // Tell the module what language to use by default
            $translateProvider.preferredLanguage('en_US');

        }])
در تکه کد فوق مشاهده می‌کنید که دو translate table زبان انگلیسی و روسی به صورت JSON وارد شده‌اند. شما قادرید تا چندین زبان را به همین صورت وارد نمایید. در خط آخر نیز زبان پیش فرض سیستم تعریف شده است.
حال به بررسی کد‌های درون کنترلر می‌پردازیم:
.controller('ctrl', ['$scope', '$translate', function ($scope, $translate) {

        $scope.tlData = {
            staticValue: 42,
            randomValue: Math.floor(Math.random() * 1000)
        };

        $scope.jsTrSimple = $translate.instant('SERVICE');
        $scope.jsTrParams = $translate.instant('SERVICE_PARAMS', $scope.tlData);

        $scope.setLang = function (langKey) {
            // You can change the language during runtime
            $translate.use(langKey);

            // A data generated by the script have to be regenerated
            $scope.jsTrSimple = $translate.instant('SERVICE');
            $scope.jsTrParams = $translate.instant('SERVICE_PARAMS', $scope.tlData);
        };

    }]);
می‌بینیم که وابستگی translate$ تزریق شده است. پس از آن دو عدد رندم به پارامتر‌های تعریف شده در translate table ارسال می‌گردد. تغییر زبان نیز توسط متد setLang صورت می‌پذیرد.
در بخش نهایی می‌خواهیم روش‌های گوناگون استفاده از translate tables را درون HTML نمایش دهیم. به بخش HTML همین مثال توجه کنید:
    <p>
        <a href="#" ng-click="setLang('en_US')">English</a>
        |
        <a href="#" ng-click="setLang('ru_RU')">Русский</a>
    </p>
    <!-- Translation by a filter -->
    <h1>{{'HEADER' | translate}}</h1>
    <!-- Translation by a directive -->
    <h2 translate="SUBHEADER">Subheader</h2>
    <!-- Using inner HTML as a key for translation -->
    <p translate>HTML_KEYS</p>
    <hr>
    <!-- Passing a data object to the translation by the filter -->
    <p>{{'DATA_TO_FILTER' | translate: tlData}}</p>
    <!-- Passing a data object to the translation by the directive -->
    <p translate="DATA_TO_DIRECTIVE" translate-values="{{tlData}}"></p>
    <hr>
    <!-- Passing a raw data to the filter -->
    <p>{{'RAW_TO_FILTER' | translate:'{ type: "raw" }' }}</p>
    <!-- Passing a raw data to the filter -->
    <p translate="RAW_TO_DIRECTIVE" translate-values="{ type: 'directives' }"></p>
    <hr>
    <!-- Using a $translate service -->
    <p>{{jsTrSimple}}</p>
    <!-- Passing a data to the $translate service -->
    <p>{{jsTrParams}}</p>
نحوه تعریف هر روش به صورت کامنت پیش از هر تگ نوشته شده است. شما به روش‌های مختلف و بر حسب استانداردهایی که خود از آن پیروی می‌کنید می‌توانید از یکی از روش‌های فوق استفاده نمایید. اما رایج‌ترین روش، دو روش اول یعنی استفاده از دایرکتیو و یا فیلتر است و دلیل آن هم سادگی و خوانا بودن این دو روش می‌باشد.

ex2_remember_language_cookies

در این مثال همانگونه که از اسم آن پیداست قصد داریم تا زبان مورد نظری را که کاربر در سیستم انتخاب نموده است، ذخیره کنیم تا پس از بستن و بازکردن مجدد وب سایت با همان زبان پیشین به کاربر نمایش داده شود. این کار بسیار ساده است. کافیست که در ابتدا علاوه بر اسکریپت‌های مثال قبل، اسکریپت‌های زیر را نیز به صفحه اضافه کنید:
    <script src="Scripts/angular-cookies.js"></script>
    <script src="Scripts/angular-translate-storage-cookie.js"></script>
با اضافه کردن خط زیر درون بدنه config، یک کوکی جدید برای شما ساخته می‌شود. این کوکی NG_TRANSLATE_LANG_KEY نام دارد که هر بار با id زبان کنونی که در translate table وارد نموده‌اید آپدیت می‌شود.
// Tell the module to store the language in the cookie
$translateProvider.useCookieStorage();
حال اگر صفحه را refresh کنید می‌بینید که زبان پیشینی که انتخاب نموده‌اید، مجددا بارگذاری می‌گردد.

ex3_remember_language_local_storage

این مثال همانند مثال قبل رفتار می‌کند، با این تفاوت که به جای اینکه کلید زبان کنونی را درون کوکی ذخیره کند، آن را درون Local Storage با نام NG_TRANSLATE_LANG_KEY قرار می‌دهد. برای اجرا کافیست اسکریپت‌ها و تکه کد زیر را با موارد مثال قبل جایگزین کنید.

<script src="Scripts/angular-translate-storage-local.js"></script>


// Tell the module to store the language in the local storage
$translateProvider.useLocalStorage();

مثال های ex4_set_a_storage_key  و ex5_set_a_storage_prefix نام کلیدی که برای ذخیره سازی زبان کنونی در کوکی یا Local Storage قرار می‌گیرد را تغییر می‌دهد که به دلیل سادگی از شرح آن می‌گذریم. 

ex6_namespace_support 

translate table در angular-translate قابلیت مفید namespacing را نیز داراست. این قابلیت به ما کمک می‌کند که جهت کپسوله کردن بخش‌های مختلف، ترجمه آنها را با namespace‌های خاص خود نمایش دهیم. به مثال زیر توجه کنید:

            $translateProvider.translations('en_US', {
                "TITLE": "How to use namespaces",
                "ns1": {
                    "HEADER": "A translations table supports namespaces.",
                    "SUBHEADER": "So you can to structurize your translation table well."
                },
                "ns2": {
                    "HEADER": "Do you want to have a structured translations table?",
                    "SUBHEADER": "You can to use namespaces now."
                }
            });

همانطور که توجه می‌کنید بخش ns1 خود شامل زیر مجموعه‌هایی است و ns2 نیز به همین صورت. هر کدام دارای کلید HEADER و SUBHEADER می‌باشند. فرض کنید هر کدام از این بخش‌ها می‌خواهند اطلاعات درون یک section را نمایش دهند. حال به نحوه‌ی فراخوانی این translate tableها دقت کنید:

<!-- section 1: Translate Table Called by ns1 namespace -->    
<h1 translate>ns1.HEADER</h1>
<h2 translate>ns1.SUBHEADER</h2>

<!-- section 2: Translate Table Called by ns2 namespace -->
<h1 translate>ns2.HEADER</h1>
<h2 translate>ns2.SUBHEADER</h2>

به همین سادگی می‌توان تمامی بخش‌ها را با namespace‌های مختلف در translate table قرار داد.

در بخش بعدی (پایانی) شش قابلیت دیگر angular translate که شامل فراخوانی translate table از یک فایل JSON، فراخوانی فایل‌های translate table به صورت lazy load و تغییر زبان بخشی از صفحه به صورت پویا هستند، بررسی خواهند شد.

فایل پروژه: AngularJs-Translate-BestPractices.zip  

نظرات نظرسنجی‌ها
با توجه به آخرین نگارش‌های موجود Angular و React، انتخاب شما برای انجام یک پروژه بزرگ کدام است؟
یک نکته‌: در اینجا شما فرض‌تان بر این است که React یک فریم‌ورک است؛ به نظرم قرار دادن React در این بین مقایسه درستی نیست؛ چون React به خودی خود یک لایبرری است به این معنا که خیلی از concernهای (routing, data fetching, ...) ساخت یک اپلیکیشن را باید خودتان هندل کنید؛ پیاده‌سازی هرکدام از این موارد به شکل صحیح دشواری‌های خاص خود را دارد؛ بنابراین بهتر است به React به عنوان یک لایبرری و یا یک معماری نگاه کنید و برای ساخت اپلیکیشن‌ها از فریم‌ورک‌هایی که مبتنی بر آن توسعه داده شده‌اند (مانند Next.js, Remix, ...) استفاده کنید.

در مورد خود نظرسنجی هم به نظرم این انتخاب کاملاً به سایز تیم بستگی دارد؛ برای پروژه‌هایی که دات‌نت هستند پیشنهاد من Blazor است (مزایای آن نیز در سایت توضیح داده شده است) به خصوص برای اعضای تیمی که به عنوان فول‌استک هستند این امر خیلی مهم است؛ در اینحالت context switching با هزینه کمتری خواهید داشت و به اصطلاح developer experience بهتری نیز خواهید داشت.
When a developer switches context, they must first disengage from the task at hand and then shift their focus to the new task. This whole process takes time and can drain devs mentally. It takes a developer 25 minutes to refocus after a context switch.