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

تا به اینجا مثال‌هایی که زده‌ایم تاثیر کامپوننت‌های React را بر روی UI، نشان دادند. در این بخش به رویداد‌های سمت UI و ورودی‌های کاربر می‌پردازیم.


رویداد‌‌های ترکیبی  React

React روش مدیریت رویداد‌های خودش را دارد و به آنها رویداد‌های Synthetic یا ترکیبی گفته میشود. در زیر مقایسه‌ای داریم از رویداد‌های معمول در JavaScript و رویدادهای React و تفاوت‌ها را بررسی میکنیم.

<!-- HTML Buttons -->
<button type="button" onclick="console.log('Button Clicked')">Click Me</button>

// React Buttons
<button type="button" onClick={console.log("Button Clicked")}>Click Me</button>
  • باید نام رویداد‌ها را بصورت camelCase تایپ کنیم. 
  • از جاوااسکریپت به طور مستقیم استفاده میکنیم؛ نه بین quotation mark‌ها.
  • برای رویداد‌ها از توابع استفاده میکنیم و بهتر است تابع اجرایی هر رویداد در خود کامپوننت ساخته شود.
  • رویداد onClick در React به نوعی override شده رویداد onclick مرورگر است و به جای آن عمل میکند.

رفتار رویداد‌های React در مرورگر‌های مختلف یکسان است. برای مثال رویداد onChange هر تغییری را برای هر نوع تگ ورودی اعمال میکند. هر کلیدی که در یک input یا textarea زده شود، اگر یک check box را انتخاب یا از انتخاب خارج کنیم و یا اگر موردی را از یک drop-down انتخاب کنیم، React رویداد onChange را اجرا میکند. React اکثر رویداد‌های مرسوم را پوشش میدهد و همچنین رویداد‌هایی را برای کار با کلیپ‌برد، رسانه‌های مختلف و تصاویر دارد. برای اطلاعات بیشتر به مستندات آن رجوع کنید.

وقتی با کتابخانه React کار میکنیم، همه چیز مجازی اتفاق می‌افتد؛ مانند ساخت تگ و نمایش آنها، همچنین مدیریت تگ‌ها و رویدادها. اما به این معنا نیست که ارتباط React با HTML DOM در مرورگر قطع است. اگر لازم باشد به HTML DOM در کامپوننت‌ها دسترسی داشته باشیم میتوانیم از خاصیت ref در React استفاده کنیم. برای مثال فرض کنید یک ورودی را برای ایمیل به‌صورت <input type="email" /> تعریف کرده‌ایم. میخواهیم پیش از ذخیره بدانیم آیا داده وارد شده به فرمت ایمیل هست یا نه. 

const EmailForm = React.createClass({
    clickHandler() {
        if (this.inputEmail.checkValidity())
            console.log("Email is OK to save it.");
        else
            console.log("Email is not in right format.");
    },
    render() {
        return (
            <div>
                <input type="email" ref={inputEmail => this.inputEmail = inputEmail} />
                <button type="submit" onClick={this.clickHandler}>Save</button>
            </div>
        )
    }

در مثال بالا clickHandler وظیفه مدیریت رویداد کلیک دکمه را به عهده دارد. در ادامه، وقتی از خاصیت ref در تگ input استفاده میکنیم و مقدار آن را یک تابع قرار میدهیم، React این تابع را زمانیکه کامپوننت به طور کامل در HTML DOM ساخته شد، اجرا میکند. React همچنین ارجاعی را به عنوان پارامتر این تابع به DOM همراه با تابع ارسال میکند (inputEmail). داخل تابع ref میتوانیم به نمونه ساخته شده از کامپوننت در DOM دسترسی داشته باشیم. inputEmail که به صورت ارجاع به تابع فرستاده شده، تگ ساخته شده input را برمیگرداند، در نتیجه میتوانیم در کامپوننت به آن دسترسی داشته باشیم.

تغییر وضعیت کامپوننت

اگر از کامپوننت‌های Sateful که دارای وضعیت هستند استفاده میکنیم، میتوانیم وضعیت کامپوننت را بر اساس ورودی‌های کاربر تغییر دهیم. مثال بالا را به این شکل تغییر میدهیم که در ابتدا وضعیت کامپوننت، یک ایمیل پیش‌فرض باشد و اگر کاربر آدرس متفاوتی را وارد کرد، آدرس جدید به عنوان وضعیت جدید کامپوننت در نظر گرفته شود. 

const EmailForm = React.createClass({
    getInitialState() {
        return {
            currentEmail: this.props.currentEmail
        }
    },
    setCurrentEmailState(se) {
        this.setState({ currentEmail: se.target.value });
    },
    clickHandler() {
        if (this.inputEmail.checkValidity())
            console.log("Email is OK to save it.");
        else
            console.log("Email is not in right format.");
    },
    render() {
        return (
            <div>
                <input type="email" ref={inputEmail => this.inputEmail = inputEmail} 
                       value={this.state.currentEmail} onChange={this.setCurrentEmailState} />
                <button type="submit" onClick={this.clickHandler}>Save</button>
            </div>
        )
    }
})

در خط 20 از مثال بالا با قرار دادن مقدار value برابر با ایمیل جاری (وضعیت کامپوننت)، کاربر آدرس پیش‌فرض را در input میبیند، اما هیچ تغییری را نمیتواند در آن ایجاد کند و input عملا تبدیل به یک تگ فقط خواندنی میشود. علت این است که React دو وضعیت را ایجاد کرده، یکی در حافظه به عنوان وضعیت پیش‌فرض و دیگری وضعیتی که در DOM ساخته. وقتی در سطح DOM تغییری را ایجاد میکنیم، React به صورت خودکار متوجه آن نمیشود و ما باید با روشی React را در جریان این تغییرات قرار دهیم! برای این کار رویداد onChange را برای تگی که قرار است تغییر کند پیاده‌سازی میکنیم. در مثال بالا متد setCurrentEmailState و رویداد onChange برای همین منظور به کار گرفته شده‌اند. 

در قسمت بعد که آخرین قسمت است، به مسئله چرخه زندگی (Lifecycle) کامپوننت‌های React میپردازیم.

مطالب
اعمال 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 را دریافت کردید ممکن است پروژه شما در مسیری طولانی در روی هارد دیسک قرار داشته باشد.
مطالب
شروع کار با Dart - قسمت 2
لطفا قسمت اول را در اینجا مطالعه بفرمائید

گام سوم: افزودن یک button
در این مرحله یک button را به صفحه html اضافه می‌کنیم. button زمانی فعال می‌شود که هیچ متنی در فیلد input موجود نباشد. زمانی که کاربر بر روی دکمه کلیک می‌کند نام Meysam Khoshbakht را در کادر قرمز رنگ می‌نویسد.
تگ <button> را بصورت زیر در زیر فیلد input ایجاد کنید
...
<div>
  <div>
    <input type="text" id="inputName" maxlength="15">
  </div>
  <div>
    <button id="generateButton">Aye! Gimme a name!</button>
  </div>
</div>
...
در زیر دستور import و بصورت top-level متغیر زیر را تعریف کنید تا یک ButtonElement در داخل آن قرار دهیم.
import 'dart:html';

ButtonElement genButton;
توضیحات
- ButtonElement یکی از انواع المنت‌های DOM می‌باشد که در کتابخانه dart:html قرار دارد
- اگر متغیری مقداردهی نشده باشد بصورت پیش فرض با null مقداردهی می‌گردد
به منظور مدیریت رویداد کلیک button کد زیر را به تابع main اضافه می‌کنیم
void main() {
  querySelector('#inputName').onInput.listen(updateBadge);
  genButton = querySelector('#generateButton');
  genButton.onClick.listen(generateBadge);
}
جهت تغییر محتوای کادر قرمز رنگ تابع top-level زیر را به piratebadge.dart اضافه می‌کنیم
...

void setBadgeName(String newName) {
  querySelector('#badgeName').text = newName;
}
جهت مدیریت رویداد کلیک button تابع زیر را بصورت top-level اضافه می‌کنیم
...

void generateBadge(Event e) {
  setBadgeName('Meysam Khoshbakht');
}
همانطور که در کدهای فوق مشاهده می‌کنید، با فشردن button تابع generateBadge فراخوانی میشود و این تابع نیز با فراخوانی تابع setBadgeName محتوای badge یا کادر قرمز رنگ را تغییر می‌دهد. همچنین می‌توانیم کد موجود در updateBadge مربوط به رویداد input فیلد input را بصورت زیر تغییر دهیم
void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
}
جهت بررسی پر بودن فیلد input می‌توانیم از یک if-else بصورت زیر استفاده کنیم که با استفاده از توابع رشته ای پر بودن فیلد را بررسی می‌کند.
void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
  if (inputName.trim().isEmpty) {
    // To do: add some code here.
  } else {
    // To do: add some code here.
  }
}
توضیحات
- کلاس String شامل توابع و ویژگی‌های مفیدی برای کار با رشته‌ها می‌باشد. مثل trim که فواصل خالی ابتدا و انتهای رشته را حذف می‌کند و isEmpty که بررسی می‌کند رشته خالی است یا خیر.
- کلاس String در کتابخانه dart:core قرار دارد که بصورت خودکار در تمامی برنامه‌های دارت import می‌شود
حال جهت مدیریت وضعیت فعال یا غیر فعال بودن button کد زیر را می‌نویسیم
void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
  if (inputName.trim().isEmpty) {
    genButton..disabled = false
             ..text = 'Aye! Gimme a name!';
  } else {
    genButton..disabled = true
             ..text = 'Arrr! Write yer name!';
  }
}
توضیحات
- عملگر cascade یا آبشاری (..)، به شما اجازه می‌دهد تا چندین عملیات را بر روی اعضای یک شی انجام دهیم. اگر به کد دقت کرده باشید با یک بار ذکر نام متغیر genButton ویژگی‌های disabled و text را مقدار دهی نمودیم که موجب تسریع و کاهش حجم کد نویسی می‌گردد.
همانند گام اول برنامه را اجرا کنید و نتیجه را مشاهده نمایید. با تایپ کردن در فیلد input و خالی کردن آن وضعیت button را بررسی کنید. همچنین با کلیک بر روی button نام درج شده در badge را مشاهده کنید.
 

گام چهارم: ایجاد کلاس PirateName

در این مرحله فقط کد مربوط به فایل dart را تغییر میدهیم. ابتدا کلاس PirateName را ایجاد می‌کنیم. با ایجاد نمونه ای از این کلاس، یک نام بصورت تصادفی انتخاب می‌شود و یا نامی بصورت اختیاری از طریق سازنده انتخاب می‌گردد.

نخست کتابخانه dart:math را به ابتدای فایل dart اضافه کنید

import 'dart:html';

import 'dart:math' show Random;

توضیحات

- با استفاده از کلمه کلیدی show، شما می‌توانید فقط کلاسها، توابع و یا ویژگی‌های مورد نیازتان را import کنید.

- کلاس Random یک عدد تصادفی را تولید می‌کند

در انتهای فایل کلاس زیر را تعریف کنید

...

class PirateName {
}

در داخل کلاس یک شی از کلاس Random ایجاد کنید

class PirateName {
  static final Random indexGen = new Random();
}

توضیحات

- با استفاده از static یک فیلد را در سطح کلاس تعریف می‌کنیم که بین تمامی نمونه‌های ایجاد شده از کلاس مشترک می‌باشد

- متغیرهای final فقط خواندنی می‌باشند و غیر قابل تغییر هستند.

- با استفاده از new می‌توانیم سازنده ای را فراخوانی نموده و نمونه ای را از کلاس ایجاد کنیم

دو فیلد دیگر از نوع String و با نام‌های _firstName و _appelation به کلاس اضافه می‌کنیم

class PirateName {
  static final Random indexGen = new Random();
  String _firstName;
  String _appellation;
}

متغیرهای خصوصی با (_) تعریف می‌شوند. Dart کلمه کلیدی private را ندارد.

دو لیست static به کلاس فوق اضافه می‌کنیم که شامل لیستی از name و appelation می‌باشد که می‌خواهیم آیتمی را بصورت تصادفی از آنها انتخاب کنیم.

class PirateName {
  ...
  static final List names = [
    'Anne', 'Mary', 'Jack', 'Morgan', 'Roger',
    'Bill', 'Ragnar', 'Ed', 'John', 'Jane' ];
  static final List appellations = [
    'Jackal', 'King', 'Red', 'Stalwart', 'Axe',
    'Young', 'Brave', 'Eager', 'Wily', 'Zesty'];
}

کلاس List می‌تواند شامل مجموعه ای از آیتم‌ها می‌باشد که در Dart تعریف شده است.

سازنده ای را بصورت زیر به کلاس اضافه می‌کنیم

class PirateName {
  ...
  PirateName({String firstName, String appellation}) {
    if (firstName == null) {
      _firstName = names[indexGen.nextInt(names.length)];
    } else {
      _firstName = firstName;
    }
    if (appellation == null) {
      _appellation = appellations[indexGen.nextInt(appellations.length)];
    } else {
      _appellation = appellation;
    }
  }
}

توضیحات

- سازنده تابعی همنام کلاس می‌باشد

- پارامترهایی که در {} تعریف می‌شوند اختیاری و Named Parameter می‌باشند. Named Parameter‌ها پارمترهایی هستند که جهت مقداردهی به آنها در زمان فراخوانی، از نام آنها استفاده می‌شود.

- تابع nextInt() یک عدد صحیح تصادفی جدید را تولید می‌کند.

- جهت دسترسی به عناصر لیست از [] و شماره‌ی خانه‌ی لیست استفاده می‌کنیم.

- ویژگی length تعداد آیتم‌های موجود در لیست را بر می‌گرداند.

در این مرحله یک getter برای دسترسی به pirate name ایجاد می‌کنیم

class PirateName {
  ...
  String get pirateName =>
    _firstName.isEmpty ? '' : '$_firstName the $_appellation';
}

توضیحات

- Getter‌ها متدهای خاصی جهت دسترسی به یک ویژگی به منظور خواندن مقدار آنها می‌باشند.

- عملگر سه گانه :? دستور میانبر عبارت شرطی if-else می‌باشد

- $ یک کاراکتر ویژه برای رشته‌های موجود در Dart می‌باشد و می‌تواند محتوای یک متغیر یا ویژگی را در رشته قرار دهد. در رشته '$_firstName the $_appellation' محتوای دو ویژگی _firstName و _appellation در رشته قرار گرفته و نمایش می‌یابند.

- عبارت (=> expr;) یک دستور میانبر برای { return expr; } می‌باشد.

تابع setBadgeName را بصورت زیر تغییر دهید تا یک پارامتر از نوع کلاس PirateName را به عنوان پارامتر ورودی دریافت نموده و با استفاده از Getter مربوط به ویژگی pirateName، مقدار آن را در badge name نمایش دهد.

void setBadgeName(PirateName newName) {
  querySelector('#badgeName').text = newName.pirateName;
}

تابع updateBadge را بصورت زیر تغییر دهید تا یک نمونه از کلاس PirateName را با توجه به مقدار ورودی کاربر در فیلد input تولید نموده و تابع setBadgeName رافراخوانی نماید. همانطور که در کد مشاهده می‌کنید پارامتر ورودی اختیاری firstName در زمان فراخوانی با ذکر نام پارامتر قبل از مقدار ارسالی نوشته شده است. این همان قابلیت Named Parameter می‌باشد.

void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  
  setBadgeName(new PirateName(firstName: inputName));
  ...
}

تابع generateBadge را بصورت زیر تغییر دهید تا به جای نام ثابت Meysam Khoshbakht، از کلاس PirateName به منظور ایجاد نام استفاده کند. همانطور که در کد می‌بینید، سازنده‌ی بدون پارامتر کلاس PirateName فراخوانی شده است.

void generateBadge(Event e) {
  setBadgeName(new PirateName());
}

همانند گام سوم برنامه را اجرا کنید و نتیجه را مشاهده نمایید.

نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
سلام

راستش من منوهای چند سطحی پویا را برای bootstrap navbar نوشتم. الگوریتمش را مجبور شدم به صورت بازگشتی بنویسم.اگر کسی می‌تونه به صورت غیر بازگشتی بگه ممنونش میشم.
این کد یه partialpage برای navbar هست که هرکسی برای bootstrap به راحتی میتونه استفاده کنه.

@model IEnumerable<DomainClasses.Page>
@helper ShowNavBar(IEnumerable<DomainClasses.Page> pages)
{
    
    foreach (var page in pages)
    {
        if (page != null)
        {
            if (page.Children.Count == 0)
            {
                <text><li><a tabindex="-1" href="#">@page.Title</a></li></text>
            }

            if (page.Children.Count > 0 && page.Parent == null)
            {         
                <text><li class="dropdown"><a class="dropdown-toggle" id="dLabel" role="button" data-toggle="dropdown" data-target="#" href="/page.html">@page.Title<b class="caret"></b></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li><a tabindex="-1" href="#">@page.Title</a></li></text>
                @ShowNavBar(page.Children)
                @:</ul></li>
            }

            if (page.Children.Count > 0 && page.Parent != null)
            {         
                <text><li class="dropdown-submenu"><a tabindex="-1" href="#">@page.Title</a><ul class="dropdown-menu"></text>
                @ShowNavBar(page.Children)
                @:</ul></li>
            }

        }
    }
    
}
<div class="navbar" style="margin-bottom: 10px;">
    <div class="navbar-inner">
        <a class="brand" href="www.google.com">IT-EBOOK</a>
        <ul class="nav">
            <li class="active"><a href="#">خانه</a></li>
            <li><a href="#">ورود</a></li>
            @ShowNavBar(Model)
            <li><a href="#">ارتباط با ما</a></li>
        </ul>
        <div class="input-append pull-left visible-desktop" style="margin-top: 5px;">
            <input class="span6 search-input" id="Text1" type="text">
            <button class="btn btn-primary" type="button">جست و جو</button>
            <button class="btn btn-info btn-advanced-search" type="button">پیشرفته</button>
        </div>
    </div>
</div>



الان تنها مشکلم اینه که فیلد‌های اضافی هم کوئری گرفته میشه.میدونم فیلدهای اضافی(بر اساس مدلی که ذکر کردم) را چگونه با استفاده از select حذف کنم اما توی viewmodel نمیدونم چه جوری children را از اطلاعات پر کنم؟
 ممنون


 
مطالب
Blazor 5x - قسمت ششم - مبانی Blazor - بخش 3 - چرخه‌های حیات کامپوننت‌ها
در این قسمت می‌خواهیم انواع رویدادهای چرخه‌ی حیات یک کامپوننت را بررسی کنیم. به همین جهت ابتدا دو کامپوننت جدید Lifecycle.razor و Lifecycle‍Child.razor را به مثالی که تا این قسمت تکمیل کرده‌ایم، اضافه کرده و آن‌ها‌را به صورت زیر جهت نمایش رویدادهای چرخه‌ی حیات، تغییر می‌دهیم:

کدهای کامل کامپوننت Pages\LearnBlazor\Lifecycle.razor
@page "/lifecycle"
@using System.Threading

<div class="border">
    <h3>Lifecycles Parent Component</h3>

    <div class="border">
        <LifecycleChild CountValue="CurrentCount"></LifecycleChild>
    </div>

    <p>Current count: @CurrentCount</p>

    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    <br /><br />
    <button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount
</div>

@code
{
    int CurrentCount = 0;
    int MaxCount = 5;

    private void IncrementCount()
    {
        CurrentCount++;
        Console.WriteLine("Parnet - IncrementCount is called");
    }

    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("Parnet - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Parnet - ShouldRender is called");
        return true;
    }

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}

و کدهای کامل کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\Lifecycle‍Child.razor
<h3 class="ml-3 mr-3">Lifecycles Child Componenet</h3>

@code
{
    [Parameter]
    public int CountValue { get; set; }

    protected override void OnInitialized()
    {
        Console.WriteLine("  Child - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("  Child - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == true) is called");
        }
        else
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("  Child - ShouldRender is called");
        return true;
    }
}
و همچنین برای دسترسی به آن‌ها، مدخل منوی زیر را به کامپوننت Shared\NavMenu.razor اضافه می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="lifecycle">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Lifecycles
    </NavLink>
</li>
با توجه به اینکه برنامه‌ی جاری از نوع Blazor Server است، Console.WriteLine‌های آن، در صفحه‌ی کنسول اجرای برنامه ظاهر می‌شوند و نه در developer tools مرورگر:





رویدادهای OnInitialized و OnInitializedAsync

@code
{
    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }
همانطور که در تصویر فوق نیز ملاحظه می‌کنید، اولین رویدادی که فراخوانی می‌شود، OnInitialized نام دارد و پس از آن نمونه‌ی async آن به نام OnInitializedAsync. این رویدادها زمانیکه یک کامپوننت و اجزای UI آن کاملا بارگذاری شده‌اند، فراخوانی می‌شوند. مهم‌ترین کاربرد آن‌ها، دریافت اطلاعات از سرویس‌های برنامه‌است.
در کامپوننت Lifecycle.razor، یک کامپوننت دیگر نیز به نام Lifecycle‍Child.razor فراخوانی شده‌است. در این حالت ابتدا OnInitialized کامپوننت والد فراخوانی شده‌است و پس از آن بلافاصله فراخوانی OnInitialized کامپوننت فرزند را مشاهده می‌کنیم.


رویدادهای OnParametersSet و OnParametersSetAsync

این رویدادها یکبار در زمان بارگذاری اولیه‌ی کامپوننت و بار دیگر هر زمانیکه کامپوننت فرزند، پارامتر جدیدی را از طریق کامپوننت والد دریافت می‌کند، فراخوانی می‌شوند. برای نمونه کامپوننت LifecycleChild، پارامتر CurrentCount را از والد خود دریافت می‌کند:
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
هرچند این پارامتر در UI کامپوننت فرزند مثال تهیه شده استفاده نمی‌شود، اما مقدار دهی آن از طرف والد، سبب بروز رویدادهای OnParametersSet و OnParametersSetAsync خواهد شد. برای آزمایش آن اگر بر روی دکمه‌ی click me در کامپوننت والد کلیک کنیم، این رویدادهای جدید را مشاهده خواهیم کرد:
Parnet - IncrementCount is called
Parnet - ShouldRender is called
  Child - OnParameterSet is called
  Child - ShouldRender is called
Parnet - OnAfterRender(firstRender == false) is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnParametersSetAsync is called
  Child - ShouldRender is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnAfterRenderAsync is called
Parnet - OnAfterRenderAsync is called
  Child - OnAfterRenderAsync is called
ابتدا متد IncrementCount کامپوننت والد فراخوانی شده‌است که سبب تغییر مقدار پارامتر CurrentCount ارسالی به کامپوننت Lifecycle‍Child می‌شود و پس از آن، رویداد OnParameterSet کامپوننت فرزند را مشاهده می‌کنید که عکس العملی است به این تغییر مقدار. یکی از کاربردهای آن، دریافت مقدار جدید پارامترهای کامپوننت و سپس به روز رسانی قسمت خاصی از UI بر اساس آن‌ها است.



رویدادهای OnAfterRender و OnAfterRenderAsync

پس از هر بار رندر کامپوننت، این متدها فراخوانی می‌شوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آن‌ها به پایان رسیده‌است. یکی از کاربردهای آن، آغاز کامپوننت‌های جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.

یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمی‌شوند؛ چون در مرحله‌ی آخر رندر UI قرار دارند.

@code
{
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }
}
در مثال‌های فوق، پارامتر firstRender را نیز مشاهده می‌کنید. یک کامپوننت چندین بار می‌تواند رندر شود. برای مثال هربار که توسط رویدادگردانی مقدار فیلدی را که در UI استفاده می‌شود، تغییر دهیم، کامپوننت مجددا رندر می‌شود. برای نمونه با کلیک بر روی دکمه‌ی click me، سبب تغییر مقدار فیلد CurrentCount می‌شویم. این تغییر و فراخوانی ضمنی StateHasChanged در پایان کار متد و در پشت صحنه، سبب رندر مجدد UI شده و در نتیجه‌ی آن، مقدار جدیدی را در صفحه مشاهده می‌کنیم. در اینجا اگر خواستیم بدانیم که رندر انجام شده برای بار اول است که صورت می‌گیرد یا خیر، می‌توان از پارامتر firstRender استفاده کرد.

سؤال: با توجه به مقدار دهی‌های 111 و 999 صورت گرفته‌ی در متد OnAfterRender، در اولین بار نمایش کامپوننت، چه عددی به عنوان CurrentCount نمایش داده می‌شود؟
در اولین بار نمایش صفحه، لحظه‌ای عدد 111 و سپس عدد 999 نمایش داده می‌شود. عدد 111 را در بار اول رندر و عدد 999 را در بار دوم رندر که پس از مقدار دهی پارامتر کامپوننت فرزند است، می‌توان مشاهده کرد.
اما ... اگر پس از نمایش اولیه‌ی صفحه، چندین بار بر روی دکمه‌ی click me کلیک کنیم، همواره عدد 1000 مشاهده می‌شود. علت اینجا است که تغییرات مقادیر فیلدها در متد OnAfterRender، به UI اعمال نمی‌شوند؛ چون در این مرحله، رندر UI به پایان رسیده‌است. در اینجا فقط مقدار فیلد CurrentCount به 999 تغییر می‌کند و به همین صورت باقی می‌ماند. دفعه‌ی بعدی که بر روی دکمه‌ی click me کلیک می‌کنیم، یک واحد به آن اضافه شده و اکنون است که کار رندر UI، مجددا شروع خواهد شد (در واکشن به یک رخ‌داد و فراخوانی ضمنی StateHasChanged در پشت صحنه) و اینبار حاصل 999+1 را در UI مشاهده می‌کنیم و باز هم در پایان کار رندر، مجددا مقدار CurrentCount به 999 تغییر می‌کند که ... دیگر به UI منعکس نمی‌شود تا زمان کلیک بعدی و همینطور الی آخر.


رویدادهای StateHasChanged و ShouldRender

- اگر خروجی رویداد ShouldRender مساوی true باشد، اجازه‌ی اعمال تغییرات به UI داده خواهد شد و برعکس. بنابراین اگر حالت UI تغییر کند و خروجی این متد false باشد، این تغییرات نمایش داده نخواهند شد.
- اگر رویداد StateHasChanged فراخوانی شود، به معنای درخواست رندر مجدد UI است. کاربرد آن در مکان‌هایی است که نیاز به اطلاع رسانی دستی تغییرات UI وجود دارد؛ درست پس از زمانیکه رندر UI به پایان رسیده‌است. برای آزمایش این مورد و فراخوانی دستی StateHasChanged، کدهای تایمر زیر تهیه شده‌اند:
@page "/lifecycle"
@using System.Threading

button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount

@code
{
    int MaxCount = 5;

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}
تایمر تعریف شده، یک thread timer است. یعنی callback آن بر روی یک ترد جدید و مجزای از ترد UI اجرا می‌شود. در این حالت اگر StateHasChanged را جهت اطلاع رسانی تغییر حالت UI فراخوانی نکنیم، در حین کار تایمر، هیچ تغییری را در UI مشاهده نخواهیم کرد.


یک نکته: متدهای رویدادگردان در Blazor، می‌توانند sync و یا async باشند؛ مانند متدهای OnClick و OnClickAsync زیر که هر دو پس از پایان متدها، سبب فراخوانی ضمنی StateHasChanged نیز می‌شوند (به این دلیل است که با کلیک بر روی دکمه‌ای، UI هم به روز رسانی می‌شود). البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged می‌شوند؛ یکبار زمانیکه قسمت sync متد به پایان می‌رسد و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید:
<button @onclick="OnClick">Synchronous</button>
<button @onclick="OnClickAsync">Asynchronous</button>
@code{
    void OnClick()
    {
    } // StateHasChanged is called after the method

    async Task OnClickAsync()
    {
        text = "click1";
        // StateHasChanged is called here as the synchronous part of the method ends

        await Task.Delay(1000);
        await Task.Delay(2000);
        text = "click2";
    } // StateHasChanged is called after the method
}
بنابراین یکی دیگر از دلایل نیاز به فراخوانی صریح InvokeAsync(StateHasChanged) در callback تایمر تعریف شده، عدم فراخوانی خودکار آن، در پایان کار رویداد callback تایمر است.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-06.zip
مطالب
تعامل و انتقال اطلاعات بین کامپوننت‌ها در Angular – بخش دوم
در قسمت قبل نحوه انتقال اطلاعات از کامپونت پدر به فرزند را از طریق متادیتای Input@ برسی کردیم. در اینجا نکات تکمیلی را مورد بحث قرار خواهیم داد.
همانطور که قبلا مشاهده کردید، نام متغیر تعریف شده در کامپوننت فرزند (FormIsReadOnly) به عنوان یک خصوصیت در هنگام استفاده از کامپوننت ظاهر شده و عمل انقیاد از طریق این خصوصیت FormIsReadOnly صورت می‌گیرد. در صورتیکه قصد دارید نام خصوصیت ظاهر شده در کامپوننت، با نام متغیر تعریف شده در کامپوننت فرزند متفاوت باشد، به شکل زیر عمل کنید. 
@Input('readOnly') FormIsReadOnly: boolean;
در این حالت عمل انقیاد از طریق خصوصیتی با نام readOnly صورت خواهد گرفت.

ردیابی تغییرات اعمال شده بر روی خصوصیت 

در برخی موارد لازم است بعد از انتساب مقداری از سمت کامپوننت پدر به کامپوننت فرزند و قبل از استفاده کامپوننت فرزند از آن مقدار، تغییرات یا اعتبار سنجی بر روی مقدار منتسب شده اعمال کنیم. مثلا فرض کنید کامپوننتی را به نام LeftSideMenu تعریف کرده‌اید که باز بودن یا بسته بودن آن توسط کامپوننت پدر تنظیم میشود. در اینجا لازم است همواره منتظر تغییر این خصوصیت از سمت کامپوننت پدر بود تا بلافاصله بعد از تنظیم این خصوصیت، کامپوننت فرزند نسبت به باز شدن یا بسته ماندن، عکس العمل نشان داده و بلافاصله تغییرات را اعمال کند (منو را باز کند یا ببندد). لازمه این کار ردیابی تغییرات اعمال شده از سمت کامپوننت پدر می‌باشد تا به محض تغییر، اصلاحات یا اعتبار سنجی‌های لازم بر روی آن اعمال شود. برای این کار دو راه حل وجود خواهد داشت. 
  1. ردیابی تغییرات صورت گرفته از طریق تنظیم setter به متغیر تعریف شده با متادیتای Input@
  2. پیاده سازی onChanges توسط کامپوننت فرزند جهت ردیابی تغییرات کامپوننت 


ردیابی تغییرات از طریق تنظیم setter

همانطور که گفته شد استفاده از کامپوننت فرزند به شکل زیر:
<app-customer-info FormIsReadOnly="true"></app-customer-info>
باعث خواهد شد مقدار انتساب یافته به FormIsReadOnly از جنس رشته‌ای باشد (یعنی "true"). در اینجا می‌خواهیم قبلا از اینکه مقدار، از طریق کامپوننت پدر به فرزند مقدار دهی شود، برسی کنیم در صورتیکه مقدار انتسابی از جنس boolean نبود، خطایی را صادر و برنامه نویس را برای این استفاده نادرست از کامپوننت هشیار کنیم: 
@Component({
    selector: 'app-customer-info',
    templateUrl: './customer-info.component.html',
    styleUrls: ['./customer-info.component.css']
})
export class CustomerInfoComponent implements OnInit {
    private _formIsReadOnly: boolean;

    @Input()
    set FormIsReadOnly(value: boolean) {
        if (typeof (value) != 'boolean')
            throw new Error(`${value} type is not boolean.`);
        this._formIsReadOnly = value;
    }

    get FormIsReadOnly(): boolean { return this._formIsReadOnly; }

    constructor() { }

    ngOnInit() {
    }
}
با تنظیم setter بر روی متغیر FormIsReadOnly، لازمه‌ی تمامی تغییرات بر روی این متغیر، اجرای آن setter خواهد بود. در اینجا برسی کردیم در صورتیکه نوع مقدار (typeof(value)) از جنس boolean نباشد، خطایی صادر شود. 


پیاده سازی onChanges توسط کامپوننت فرزند جهت ردیابی تغییرات کامپوننت 

یکی دیگر از راه‌های تشخیص تغییرات اعمال شده بر روی کامپوننت، پیاده سازی اینترفیس onChanges توسط کامپوننت و پیاده سازی متد تعریف شده در این اینترفیس به نام ngOnChanges می‌باشد. 
import { Component, OnInit, Input, OnChanges, SimpleChange } from '@angular/core';
import { ICustomerInfo } from '../../core/model/ICustomerInfo';

@Component({
    selector: 'app-customer-info',
    template: '<ul>< li *ngFor="let change of changeLog">{{change }}</li></ul>',
    styleUrls: ['./customer-info.component.css']
})
export class CustomerInfoComponent implements OnChanges {
    changeLog: string[] = [];

    @Input() FormIsReadOnly: boolean;

    ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
        let log: string[] = [];
        for (let propName in changes) {
            let changedProp = changes[propName];
            let to = JSON.stringify(changedProp.currentValue);
            if (changedProp.isFirstChange()) {
                log.push(`Initial value of ${propName} set to ${to}`);
            } else {
                let from = JSON.stringify(changedProp.previousValue);
                log.push(`${propName} changed from ${from} to ${to}`);
            }
        }
        this.changeLog.push(log.join(', '));
    }

    constructor() { }
}
این کامپوننت تمامی تغییرات اعمال شده بر روی FormIsReadOnly را ردیابی کرده و نمایش خواهد داد. نمونه خروجی به شکل زیر خواهد بود. 
•    Initial value of FormIsReadOnly set to true
•    FormIsReadOnly changed from true to "trued"
•    FormIsReadOnly changed from "trued" to "true"
•    FormIsReadOnly changed from "true" to "truef"
•    FormIsReadOnly changed from "truef" to "true"
•    FormIsReadOnly changed from "true" to "tru"
•    FormIsReadOnly changed from "tru" to "tr"
•    FormIsReadOnly changed from "tr" to "t"
•    FormIsReadOnly changed from "t" to ""
•    FormIsReadOnly changed from "" to "t"
•    FormIsReadOnly changed from "t" to "tr"
•    FormIsReadOnly changed from "tr" to "tru"
•    FormIsReadOnly changed from "tru" to "true"

در ادامه عنوان «به‌جریان انداختن رخدادها از کامپوننت فرزند و گرفتن آن‌ها را از طریق کامپوننت پدر» را مورد برسی قرار خواهیم داد.


ادامه دارد/

مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت دوم - تعیین نوع‌های پیشرفته‌تر props
در قسمت قبل با معرفی نوع props توسط TypeScript، مجبور به تکمیل اجباری تک تک آن‌ها شدیم؛ اما در React می‌توان props را به صورت اختیاری و یا با مقادیری پیش‌فرض نیز تعریف کرد.


روش تعیین props پیش‌فرض توسط TypeScript

اگر بخواهیم توسط روش‌های خود React، مقادیر پیش‌فرض props را تعیین کنیم، می‌توان از defaultProps به صورت زیر با تعریف یک شیء جاوا اسکریپتی از پیش مقدار دهی شده، استفاده کرد:
Head.defaultProps = {
   title: "Hello",
   isActive: true
};
اما در حالت استفاده‌ی از TypeScript و یا حتی نگارش ES6 آن (React در حالت پیش‌فرض آن)، می‌توان مقادیر پیش‌فرض props را با مقدار دهی مستقیم متغیرهای حاصل از Object Destructuring آن، تعیین کرد:
type Props = {
  title: string;
  isActive: boolean;
};

export const Head = ({ title = "Hello", isActive = true }: Props) => {
در اینجا هر متغیری که با مقداری پیش‌فرض، مقدار دهی شده باشد، اختیاری در نظر گرفته شده و اگر دارای مقدار پیش‌فرضی نباشد، باید به صورت اجباری در حین تعریف المان این کامپوننت، ذکر شود.
در این حالت انتظار داریم که در حین استفاده و تعریف المان کامپوننت Head، اگر برای مثال ویژگی isActive را ذکر نکردیم، کامپایلر TypeScript خطایی را گزارش نکند؛ که اینطور نیست:


هنوز هم در اینجا می‌توان خطای عدم تعریف خاصیت isActive را مشاهده کرد. برای رفع این مشکل، به صورت زیر عمل می‌کنیم:
type Props = {
  title: string;
  isActive?: boolean;
};

export const Head = ({ title, isActive = true }: Props) => {
در حین تعریف یک type، اگر خاصیتی با علامت ? ذکر شود، به معنای اختیاری بودن آن است. همچنین در اینجا مقدار پیش‌فرض title را هم حذف کرده‌ایم تا تعریف آن اجباری شود. بنابراین در typeها، تمام خواص اجباری هستند؛ مگر اینکه توسط ? به صورت اختیاری تعریف شوند. این مورد هم مزیتی است که در ابتدای طراحی props یک کامپوننت، باید در مورد اختیاری و یا اجباری بودن آن‌ها بیشتر فکر کرد. همچنین نیازی به استفاده از روش‌های غیراستانداردی مانند Head.defaultProps خود React نیست. ذکر مقدار پیش‌فرض متغیرهای حاصل از Object Destructuring، جزئی از جاوااسکریپت استاندارد است و یا مشخص سازی خواص اختیاری در TypeScript، فقط مختص به پروژه‌های React نیست و در همه جا به همین شکل کاربرد دارد.
اکنون با تعریف isActive?: boolean، دیگر شاهد نمایش خطایی در حین تعریف المان Head، بدون ذکر خاصیت isActive، نخواهیم بود.


تعریف انواع و اقسام نوع‌های props

تا اینجا نوع‌های ساده‌ای مانند string و boolean و همچنین نحوه‌ی تعریف اجباری و اختیاری آن‌ها را بررسی کردیم. در ادامه یک نمونه‌ی کامل‌تر را مشاهده می‌کنید:
type User = {
  name: string;
};

type Props = {
  title: string;
  isActive?: boolean;
  count: number;
  options: string[];
  status: "loading" | "loaded";
  thing: {};
  thing2: {
    name: string;
  };
  user: User;
  func: () => void;
};
- در ابتدا نوع‌های متداولی مانند number و string ذکر شده‌اند.
- سپس نحوه‌ی تعریف آرایه‌ای از رشته‌ها را مشاهده می‌کنید.
- یا می‌توان مقدار یک خاصیت را تنها به مقادیری خاص محدود کرد؛ مانند خاصیت status در اینجا و اگر در حین مقدار دهی این خاصیت، از مقدار دیگری استفاده شود، تایپ‌اسکریپت، خطایی را صادر می‌کند.
- در ادامه سه روش تعریف اشیاء تو در تو را مشاهده می‌کنید؛ خاصیت thing از نوع یک شیء خالی تعریف شده‌است (بجای آن می‌توان از نوع object هم استفاده کرد). خاصیت thing2 از نوع یک شیء که دارای خاصیت رشته‌ای name است، تعریف شده و یا بهتر است این نوع تعاریف را به یک type مستقل دیگر مانند User منتقل کرد و سپس از آن جهت تعیین نوع خاصیتی مانند user استفاده نمود.
- در اینجا حتی می‌توان یک خاصیت را که از نوع یک تابع است، تعریف کرد. در این تعریف، void نوع خروجی آن است.


روش تعریف props تابعی در TypeScript

برای بررسی روش تعریف نوع توابع ارسالی از طریق props، ابتدا کامپوننت جدید src\components\Button.tsx را ایجاد می‌کنیم. سپس آن‌را به صورت زیر تکمیل خواهیم کرد:
import React from "react";

type Props = {
  // onClick(): string;  method returns string
  // onClick(): void  method returns nothing;
  // onClick(text: string): void; method with params
  // onClick: () => void; function returns nothing
  onClick: (text: string) => void; // function with params
};

export const Button = ({ onClick }: Props) => {
  return <button onClick={() => onClick("hi")}>Click Me</button>;
};
در این کامپوننت، متغیر onClick حاصل از Object Destructuring شیء props دریافتی، یک تابع است که قرار است با کلیک بر روی دکمه‌ای که در این کامپوننت قرار دارد، پیامی را به کامپوننت والد ارسال کند.
با توجه به تعریف { onClick }، در همان لحظه، خطای any بودن نوع آن از طرف TypeScript گزارش داده می‌شود. بنابراین نوع جدید Props را ایجاد کرده و برای onClick، نوع متناسبی را تعریف می‌کنیم. در اینجا 4 روش مختلف تعریف نوع function را در TypeScript مشاهده می‌کنید؛ دو حالت آن با ذکر پرانتزها و درج امضای متد انجام شده و دو حالت دیگر به کمک arrow functions پیاده سازی شده‌اند.
برای نمونه آخرین حالت تعریف شده از روش arrow functions استفاده می‌کند که متداول‌ترین روش تعریف نوع توابع است (چون عنوان می‌کند که نوع onClick، یک تابع است و آن‌را شبیه به یک متد معمولی نمایش نمی‌دهد) که در آن در ابتدا امضای پارامترهای این تابع مشخص شده‌اند و در ادامه پس از <=، نوع خروجی این تابع تعریف شده‌است که void می‌باشد (این تابع چیزی را بر نمی‌گرداند).

در آخر، تعریف المان آن‌را به صورت زیر به فایل src\App.tsx اضافه می‌کنیم که onClick آن یک مقدار را دریافت کرده و سپس آن‌را در کنسول نمایش می‌دهد.
البته خروجی از نوع void، در اینجا بسیار معمول است؛ چون هدف از این نوع توابع بیشتر ارسال مقادیری به کامپوننت در برگیرنده‌ی آن‌ها است (مانند value در اینجا) و اگر برای مثال خروجی رشته‌ای را داشته باشند، باید در حین درج و تعریف المان آن‌ها، برای نمونه یک "return "value1 را هم در انتهای کار قرار داد که عملا استفاده‌ای ندارد و بی‌معنا است:
import { Button } from "./components/Button";
import { Head } from "./components/Head";
// ...

function App() {
  return (
    <div className="App">
      <Head title="Hello" />
      <Button
        onClick={(value) => {
          console.log(value);
        }}
      />
  // ...
مطالب
روش های ارث بری در Entity Framework - قسمت دوم
در قسمت اول در مورد روش TPT خواندید. در این قسمت به روش TPH می‌پردازیم.

روش TPH
در این روش، ارث بری از طریق فقط یک جدول ایجاد می‌شود و زیر مجموعه‌ها بر اساس مقدار یک فیلد از یکدیگر متمایز می‌شوند. پس اگر جدولی دارید که برای متمایز کردن رکوردهای آن از یک فیلد استفاده می‌کنید، روش TPH مناسب شما است. با روش TPH نیز می‌توانید به همان مدلی که در روش TPT دارید برسید، تنها تفاوت این هست که در روش TPH، تمامی داده‌ها در یک جدول قرار دارند و یک فیلد برای متمایز کردن رکوردها استفاده می‌شود. همه چیز با مثال عملی واضح‌تر است. پس کار خود را با یک مثال ادامه می‌دهیم. جدول مثال ما در شکل زیر مشخص است.


به نظر می‌رسد که این جدول با جداول قسمت قبل شباهتی دارد. بله! فیلدهای جداول مثال قبل در این جدول آمده اند.
  • فیلدهای FirstName و LastName از جدول Persons
  • فیلد HireDate از جدول Instructors
  • فیلد EnrollmentDate، Credits و Degree از جدول Students
  • فیلد AdminDate از جدول Admins
  • فیلدهای BusinessCredits و Discipline از جدول BusinessStudents

یک فیلد با نام PersonCategory نیز اضافه شده است که «مقداری عددی» می‌پذیرد و برای «متمایز کردن رکوردها» استفاده می‌شود:

  • 1 ، نمایانگر Student
  • 2 ، نمایانگر Instructor
  • 3 ، نمایانگر Admin
  • 4 ، نمایانگر BusinessStudent

از این جدول می‌خواهیم به مدل قسمت اول برسیم اما این بار با استفاده از روش TPH. در شکل زیر، جدول Persons به صورت مدل شده در برنامه نشان داده شده است.

حال باید چهار موجودیت مشتق شده را به مدل اضافه کنیم. موجودیت‌های Student، Instructor و Admin را با کلیک راست بر روی EDM Designer و انتخاب گزینه‌ی Entity از منوی Add New ایجاد کنید. در فرمی که باز می‌شود، از قسمت  Person ،Base type را انتخاب کنید. موجودیت BusinessStudent را نیز ایجاد و موجودیت پایه‌ی آن را موجودیت Student در نظر بگیرید. مدل ایجاد شده تا اینجای کار در شکل زیر مشخص است.

حال باید خصیصه‌های موجودیت Person را به موجودیت‌های مشتق شده منتقل کرد. بدین منظور، هر خصیصه از موجودیت Person را انتخاب، کلیدهای Ctrl+X را فشار دهید، سپس بر روی قسمت Properties موجودیت مشتق شده‌ی مورد نظر رفته و کلیدهای Ctrl+V را فشار دهید. نتیجه در شکل زیر نشان داده شده است.

اکنون زمان آن رسیده است تا جدول متناظر با هر یک از موجودیت‌های مشتق شده را معرفی کنیم. تمامی موجودیت‌های مشتق شده از جدول Persons استفاده می‌کنند. بر روی هر یک از آنها کلیک راست کرده و گزینه‌ی Table Mapping را انتخاب کنید. پنجره  ی Mapping Details نشان داده می‌شود. ابتدا بر روی عبارت Add a Table or View و سپس بر روی نشانگر رو به پایینی که کنار آن ظاهر می‌شود کلیک کنید (شکل زیر). 

آیتم Persons را انتخاب کنید. اکنون باید فیلد تفکیک کننده‌ی رکوردها را مشخص کنیم. برای این حالت باید یک شرط ایجاد نمود. در همان پنجره‌ی Mapping Details، عبارتی با عنوان Add a Condition وجود دارد. بر روی آن کلیک و در لیستی که ظاهر می‌شود، آیتم PersonCategory را انتخاب کنید (شکل زیر).

سپس در ستون Value/Property، مقدار آن را "1" قرار دهید (شکل زیر).

تناظر میان موجودیت و جدول Persons و مقداردهی مناسب به فیلد متمایز کننده را برای تمامی موجودیت‌های مشتق شده انجام دهید. دلیل این کار این است که EF بداند هر رکورد در چه زمانی باید به چه موجودیتی تبدیل شود. دقت کنید که پیشتر به مقدار فیلد متمایز کننده برای هر موجودیت اشاره کردیم. نکته‌ی مهم اینکه یک شرط نیز باید برای موجودیت Person ایجاد و مقدار فیلد متمایز کننده‌ی آن را "صفر" تعریف کنید.
مثال ما آماده است. آن را امتحان می‌کنیم.

using (PersonDbEntities context = new PersonDbEntities())
{

    var people = from p in context.Persons
                 select p;

    foreach (Person person in people)
    {
        Console.WriteLine("{0}, {1}",
            person.LastName,
            person.FirstName);

        if (person is Student)
            Console.WriteLine("    Degree: {0}",
                ((Student)person).Degree);
        
        if (person is BusinessStudent)
            Console.WriteLine("    Discipline: {0}",
                ((BusinessStudent)person).Discipline);
    }

    Console.ReadLine();
}
چه کدهای آشنایی! بله، این کدها همان کدهایی هستند که برای مثال روش TPT استفاده کردیم و بدون کوچکترین تغییری در اینجا نیز قابل استفاده هستند.

مزایای روش TPH
  • سرعت بالای عملیات CRUD، به دلیل وجود تمامی داده‌ها در یک جدول
  • تعداد جداول در پایگاه داده، کم و مدیریت آنها آسان‌تر است

معایب روش TPH
  • افزونگی داده ها. مقادیر برخی ستون‌ها برای بعضی از رکوردها، حاوی مقدار NULL است و تعداد این ستون‌ها به تعداد زیر مجموعه‌ها ارتباط دارد
  • عیب اول، باعث می‌شود تا در صورتی که داده‌ها به صورت دستی تغییر پیدا کنند، جامعیت داده‌ها از بین برود
  • افزایش بی دلیل حجم داده ها
  • اضافه و حذف کردن موجودیت‌ها به مدل، عملی زمانبر و پیچیده است
سومین روش، TPC است که در EF Designer، پشتیبانی از پیش تعبیه شده برای آن وجود ندارد و باید از طریق ویرایش فایل edmx. به آن رسید. استفاده از این روش توصیه نمی‌شود.

مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت یازدهم
در این قسمت قصد بررسی کامپوننت‌های فوق العاده‌ی Syncfusion را داریم. احتمالا Syncfusion را با کتاب‌های Succinctly Series آن می شناسید. این شرکت برای Xamarin Forms نزدیک به 130‌ کامپوننت، شامل موارد کار با دیتا، اعم از فرم‌های Data Entry ،Data Grid و ListView را نوشته که در کنار کنترل‌های کار با فایل‌های Office-PDF و همچنین گزارشات و چارت‌ها و سایر کنترل‌های آن، نیاز هر برنامه‌ای را برآورده می‌کند. یکی از چند ده کتاب Xamarin Forms نیز توسط این شرکت نوشته شده‌است.
ما ضمن استفاده کامپوننت List View آن، هم کار با List View را یاد می‌گیریم و هم کار با Syncfusion را. در نظر داشته باشید که خود Xamarin Forms نیز List View دارد و در نسخه‌ی 4 آن که هم اکنون در مرحله‌ی Preview است، کنترل جدیدی به نام CollectionView نیز ارائه شده که امکانات خیلی خوبی دارد.
توجه: مطالب آموزش زیر، از این لینک آورده شده‌است.

برای شروع، ابتدا Nuget Package مربوطه را در پروژه‌ی XamApp نصب کنید. نیازی به نصب کردن آن در XamApp.Android و XamApp.iOS و XamApp.UWP نیست، ولی باز و بسته کردن ویژوال استودیو بعد از نصب و راست کلیک کردن بر روی Solution و زدن Restore nuget packages ایده‌ی خوبی است!

در پروژه‌ی iOS، در فایل AppDelegate.cs، بعد از Forms.Init، کد زیر را کپی کنید:

SfListViewRenderer.Init();

همین کد را در MainPage.xaml.cs در پروژه UWP، قبل از LoadApplication قرار دهید. نیازی به انجام کاری در Android نیست.

سپس Product Key این محصول را به دست آورده و در پروژه XamApp، فولدر Views در فایل SyncfusionLicense قرار دهید.

حال برای نمایش لیستی از محصولات، ابتدا کلاس Product را ایجاد می‌کنیم. چه در زمانیکه یک Rest api را در سمت سرور فراخوانی می‌کنیم و چه زمانیکه با دیتابیس بر روی گوشی یعنی Sqlite کار می‌کنیم، در نهایت لیستی از یک کلاس را داریم (در اینجا Product).

    public class Product : Bindable
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsActive { get; set; }
        public decimal Price { get; set; }
    }

در یک View Model جدید با نام ProductsViewModel، در OnNavigatedToAsync، دیتا را از سرور یا دیتابیس، بر روی گوشی دریافت می‌کنیم؛ اما در این مثال، برای راحتی بیشتر یک List را New می‌کنیم:

    public class ProductsViewModel : BitViewModelBase
    {
        public List<Product> Products { get; set; }

        public async override Task OnNavigatedToAsync(INavigationParameters parameters)
        {
            Products = new List<Product> // getting products from server or sqlite database
            {
                new Product { Id = 1, IsActive = true, Name = "Product1" , Price = 12.2m /* m => decimal */ },
                new Product { Id = 2, IsActive = false, Name = "Product2" , Price = 14 },
                new Product { Id = 3, IsActive = true, Name = "Product3" , Price = 11 },
            };
            await base.OnNavigatedToAsync(parameters);
        }
    }

حال نوبت به دادن یک Template می‌رسد. مثلا فرض کنید می‌خواهیم نام را درون یک Label نمایش دهیم و بر اساس فعال یا غیر فعال بودن Product، یک Checkbox را تغییر داده، تیک بزنیم یا نزنیم و در نهایت نمایش قیمت را در یک Label دیگر خواهیم داشت.

    <sfListView:SfListView ItemsSource="{Binding Products}">
        <sfListView:SfListView.ItemTemplate>
            <DataTemplate>
                <FlexLayout x:DataType="model:Product" Direction="Row">
                    <Label
                        FlexLayout.Basis="50%"
                        Text="{Binding Name}"
                        VerticalTextAlignment="Center" />
                    <bitControls:BitCheckbox InputTransparent="True" FlexLayout.Basis="25%" IsChecked="{Binding IsActive}" />
                    <Label
                        FlexLayout.Basis="25%"
                        Text="{Binding Price}"
                        VerticalTextAlignment="Center" />
                </FlexLayout>
            </DataTemplate>
        </sfListView:SfListView.ItemTemplate>
    </sfListView:SfListView>

همانطور که می‌بینید، در DataTemplate از Flex Layout استفاده شده است. Flex Layout در کنار Grid, Stack, Relative, Absolute و سایر Layout‌‌های Xamarin Forms در پروژه قابلیت استفاده دارد و مزیت‌های خاص خود را دارد.

این Data Template توسط List View، حداکثر سه بار ساخته می‌شود؛ چون View Model در لیست مثال خود، سه Product دارد. خود List View تکنیک‌های Virtualization و Cell Reuse را بدون نیاز به هیچ کد اضافه‌ای هندل می‌کند و Performance خوبی دارد. در View مربوطه یعنی ProductsView.xaml، هر Binding ای (مثل Binding Products) به View Model اشاره می‌کند، اما درون Data Template، هر Binding به Product ای اشاره می‌کند که آن ردیف List View، دارد نمایش‌اش می‌دهد. برای همین x:DataType را روی Flex Layout درون Data Template به Product وصل کرده‌ایم. در این صورت اگر بنویسیم Binding N_ame، به ما خطا داده می‌شود که کلاس Product هیچ Property با نام N_ame ندارد که خطای درستی است.

روی BitCheckbox مقدار InputTransparent را برابر با True داده‌ایم که باعث می‌شود کلیک روی Checkbox عملا در نظر گرفته نشود. این منطقی است، زیرا عوض کردن مقدار Checkbox در این مثال ما ذخیره نمی‌شود و کاربرد نمایشی دارد و فقط باعث گیج شدن کاربر می‌شود.

کنترل BitCheckbox از مجموعه کنترل‌های Bit است که اخیرا با BitDatePicker آن آشنا شده‌اید. برای آشنایی با نحوه افزودن این کنترل‌ها به یک پروژه، به مستندات Bit Framework مراجعه کنید. خود Syncfusion نیز Checkbox دارد.

حال فرض کنید که قرار است دکمه‌ای برای هر ردیف List View داشته باشیم که با زدن روی آن، اطلاعات Product به سرور ارسال شود و جزئیات بیشتری دریافت و در قالب یک Alert نمایش داده شود.  برای این کار، ابتدا به Data Template که Flex Layout است، یک دکمه اضافه می‌کنیم. سپس Command آن دکمه را به View Model بایند می‌کنیم. در آن Command البته احتیاج داریم بدانیم درخواست نمایش جزئیات بیشتر، برای کدام Product داده شده. این مهم با Command Parameter شدنی است.

برای پیاده سازی این مثال، در سمت View Model داریم:

 
public BitDelegateCommand<Product> ShowProductDetailsCommand { get; set; }
public IUserDialogs UserDialogs { get; set; } async Task ShowProductDetails(Product product) { string productDetail = $"Product: {product.Name}'s more info: ..."; // get more info from server. await UserDialogs.AlertAsync(productDetail, "Product Detail"); }

کامند ShowProductDetailCommand یک پارامتر را از جنس Product می‌گیرد و آن Product ای است که روی دکمه آن کلیک شده‌است. با Clone کردن آخرین نسخه XamApp و درخواست نمایش صفحه‌ی Products در App.xaml.cs به صورت زیر و اجرای برنامه، می‌توانید درک بهتری از عملکرد آن داشته باشید:

await NavigationService.NavigateAsync("/Nav/Products", animated: false);

سپس در View مربوطه داریم:

 
...
<Button Command="{Binding ShowProductDetailsCommand}" CommandParameter="{Binding .}" Text="Detail..." /> </FlexLayout> </DataTemplate>

CommandParameter اگر برابر با Binding Id می‌بود، به Command در سمت View Model، بجای کل Product، فقط Id آن ارسال می‌شد. ولی Show Product Detail Command منتظر یک Product کامل است، نه فقط Id آن. با نوشتن 

CommandParameter="{Binding .}"

کل Product با کلیک روی دکمه به Command ارسال می‌شود.

اکنون اگر پروژه را Build کنید، خطایی را از x:DataType خواهید گرفت که منطقی است. اگر Binding Name و Binding Price دو Property با نام‌های Name و Price را از کلاس Product جستجو می‌کنند، پس قاعدتا ShowProductDetailCommand نیز در همان کلاس مدل، یعنی Product جستجو می‌شود! ولی می‌دانیم که این Command در View Model ما یعنی ProductsViewModel است. برای حل این مشکل، به جای Binding از bit:ViewModelBinding استفاده می‌کنیم:

Command="{bit:ViewModelBinding ShowProductDetailsCommand}"

در این صورت، بجای جستجو کردن ShowProductDetailCommand در کلاس Product، این را در ProductsViewModel جستجو می‌کند که منجر به خروجی درست می‌شود.

این List View دارای امکاناتی چون Infinite loading، Pull to refresh و Grouping-Sorting-Filtering و ... است که می‌توانید از روی مستندات خوب Syncfusion، آنها را راه اندازی کنید و اگر به مشکلی برخوردید نیز اینجا بپرسید. همچنین نگاهی به لیست 129 کنترل دیگر بیاندازید و ببینید که در برنامه‌های خود از کدام یک از آنها می‌توانید استفاده کنید.

مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
Ajax.BeginForm در ASP.NET MVC از jQuery Ajax برای ارسال مقادیر فرم، به سرور استفاده می‌کند. در این بین اگر یکی از عناصر فرم، المان ارسال فایل به سرور باشد، مقدار دریافتی در سمت سرور نال خواهد بود. مشکل اینجا است که نمی‌توان به کمک Ajax معمولی (یا به عبارتی XMLHttpRequest) فایلی را به سرور ارسال کرد. یا باید از سیلورلایت یا فلش استفاده نمود و یا از مرورگرهایی که XMLHttpRequest Level 2 را پشتیبانی می‌کنند (از IE 10 به بعد مثلا) که امکان Ajax upload توکار به همراه گزارش درصد آپلود را بدون نیاز به فلش یا سیلورلایت، دارند.
در این بین راه حل دیگری نیز وجود دارد که با تمام مرورگرها سازگار است؛ اما تنها گزارش درصد آپلود را توسط آن نخواهیم داشت. در اینجا به صورت پویا یک IFrame مخفی در صفحه تشکیل می‌شود، مقادیر معمولی فرم (تمام المان‌ها، منهای file) به صورت Ajax ایی به سرور ارسال خواهند شد. المان file آن در این IFrame مخفی، به صورت معمولی به سرور Postback می‌شود. البته کاربر در این بین چیزی را مشاهده یا احساس نخواهد کرد و تمام عملیات از دیدگاه او Ajax ایی به نظر می‌رسد. برای انجام اینکار تنها کافی است از افزونه‌ی AjaxFileUpload استفاده کنیم که در ادامه نحوه‌ی استفاده از آن‌را بررسی خواهیم کرد.


پیشنیازها

در ادامه فرض بر این است که افزونه‌ی AjaxFileUpload را دریافت کرده و به فایل Layout برنامه افزوده‌اید:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.11.1.min.js"></script>
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
    <script src="~/Scripts/ajaxfileupload.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>


مدل، کنترلر و View برنامه

مدل برنامه مشخصات یک محصول است:
namespace MVCAjaxFormUpload.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

کنترلر آن از سه متد تشکیل شده‌است:
using System.Threading;
using System.Web;
using System.Web.Mvc;
using MVCAjaxFormUpload.Models;

namespace MVCAjaxFormUpload.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(Product product)
        {
            var isAjax = this.Request.IsAjaxRequest();

            return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult UploadFiles(HttpPostedFileBase image1, int id)
        {
            var isAjax = this.Request.IsAjaxRequest();
            Thread.Sleep(3000); //شبیه سازی عملیات طولانی
            return Json(new { FileName = "/Uploads/filename.ext" }, "text/html", JsonRequestBehavior.AllowGet);
        }
    }
}
Index اول، کار نمایش صفحه‌ی ارسال اطلاعات را انجام خواهد داد.
Index دوم کار پردازش Ajax ایی اطلاعات ارسالی به سرور را به عهده دارد. HttpPost آن Ajax ایی است.
متد UploadFiles، کار پردازش اطلاعات ارسالی از طرف IFrame مخفی را انجام می‌دهد. HttpPost آن معمولی است.

و کدهای View این مثال نیز به شرح زیر است:
@model MVCAjaxFormUpload.Models.Product
@{
    ViewBag.Title = "Index";
}

<h2>Ajax Form Upload</h2>

@using (Ajax.BeginForm(actionName: "Index",
                       controllerName: "Home",
                       ajaxOptions: new AjaxOptions { HttpMethod = "POST" },
                       routeValues: null,
                       htmlAttributes: new { id = "uploadForm" }))
{
    <label>Name:</label>
    @Html.TextBoxFor(model => model.Name)
    <br />
    <label>Image:</label>
    <br />
    <input type="file" name="Image1" id="Image1" />
    <br />
    <input type="submit" value="Submit" />
    <img id="loading" src="~/Content/Images/loading.gif" style="display:none;">
}

@section Scripts
{
    <script type="text/javascript">
        $(function () {
            $('#uploadForm').submit(function () {
                $("#loading").show();
                $.ajaxFileUpload({
                    url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود
                    secureuri: false,
                    fileElementId: 'Image1', // آی دی المان ورودی فایل
                    dataType: 'json',
                    data: { id: 1, data: 'test' }, // اطلاعات اضافی در صورت نیاز
                    success: function (data, status) {
                        $("#loading").hide();
                        if (typeof (data.FileName) != 'undefined') {
                            alert(data.FileName);
                        }
                    },
                    error: function (data, status, e) {
                        $("#loading").hide();
                        alert(e);
                    }
                });
            });
        });
    </script>
}
فرمی که توسط Ajax.BeginForm تشکیل شده‌است، یک فرم معمولی Ajax ایی است و نکته‌ی جدیدی ندارد. تنها در آن یک المان ارسال فایل قرار گرفته‌است و همچنین Id آن‌را نیز جهت استفاده توسط jQuery مشخص کرده‌ایم.
در ادامه نحوه‌ی فعال سازی ajaxFileUpload را دقیقا در زمان submit فرم، مشاهده می‌کنید. در اینجا url آن به اکشن متدی که اطلاعات المان file را باید دریافت کند، اشاره می‌کند. fileElementId آن مساوی Id المان فایل فرم Ajax ایی صفحه‌است. از قسمت data جهت ارسال اطلاعات اضافه‌تری به اکشن متد UploadFiles استفاده می‌شود. سایر قسمت‌های آن نیز مشخص هستند. اگر عملیات موفقیت آمیز بود، success آن و اگر خیر، error آن اجرا می‌شوند.
فقط باید دقت داشت که content type دریافتی توسط آن باید text/html باشد، که این مورد در اکشن متدهای کنترلر مشخص هستند.
به این ترتیب دیگر کاربر نیازی ندارد ابتدا یکبار بر روی دکمه‌ی دومی کلیک کرده و فایل را ارسال کند و سپس بار دیگر بر روی دکمه‌ی submit فرم کلیک نماید. هر دو کار توسط یک دکمه انجام می‌شوند.

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
MVCAjaxFormUpload.zip