مطالب
Kendo UI MVVM
پیشنیازها
- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودی‌های کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوه‌ی تعریف DataSource ایی که می‌تواند اطلاعات را ثبت، حذف و یا ویرایش کند.


در این مطلب قصد داریم به یک چنین صفحه‌ای برسیم که در آن در ابتدای نمایش، لیست ثبت نام‌های موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده می‌شود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیف‌های جدید. در این بین مدیریت نمایش لیست ثبت نام‌ها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.



Kendo UI MVVM

الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترل‌های برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شده‌است، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانه‌هایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شده‌اند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز می‌باشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل می‌شود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده می‌شود.
- ViewModel واسطی است بین Model و View. کار آن انتقال داده‌ها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح می‌باشد.
- Declarative data binding جهت رهایی برنامه نویس‌ها از نوشتن کدهای هماهنگ سازی اطلاعات المان‌های View و خواص ViewModel کاربرد دارد.

در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال می‌کنیم.


تعریف Model و ViewModel

در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models
{
    public class Registration
    {
        public int Id { set; get; }
        public string UserName { set; get; }
        public string CourseName { set; get; }
        public int Credit { set; get; }
        public string Email { set; get; }
        public string Tel { set; get; }
    }
}
در سمت کاربر، این مدل را به نحو ذیل می‌توان تعریف کرد:
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                id: "Id",
                fields: {
                    Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new.
                    UserName: { type: 'string' },
                    CourseName: { type: 'string' },
                    Credit: { type: 'number' },
                    Email: { type: 'string' },
                    Tel: { type: 'string' }
                }
            });
        });
    </script>
و ViewModel برنامه در ساده‌ترین شکل آن اکنون چنین تعریفی را خواهد یافت:
    <script type="text/javascript">
        $(function () {
            var viewModel = kendo.observable({
                accepted: false,
                course: new model()
            });
        });
    </script>
یک viewModel در Kendo UI به صورت یک observable object تعریف می‌شود که می‌تواند دارای تعدادی خاصیت و متد دلخواه باشد. هر خاصیت آن به یک عنصر HTML متصل خواهد شد. در اینجا این اتصال دو طرفه است؛ به این معنا که تغییرات UI به خواص viewModel و برعکس منتقل و منعکس می‌شوند.


اتصال ViewModel به View برنامه

تعریف فرم ثبت نام را در اینجا ملاحظه می‌کنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگی‌های خاص آن، مزین شده‌اند. جزئیات آن‌را در مطلب «اعتبار سنجی ورودی‌های کاربر در Kendo UI» پیشتر بررسی کرده‌ایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                <h3>ثبت نام</h3>
                <ul>
                    <li>
                        <label for="Id">Id</label>
                        <span id="Id" data-bind="text:course.Id"></span>
                    </li>
                    <li>
                        <label for="UserName">نام</label>
                        <input type="text" id="UserName" name="UserName" class="k-textbox"
                               data-bind="value:course.UserName"
                               required />
                    </li>
                    <li>
                        <label for="CourseName">دوره</label>
                        <input type="text" dir="ltr" id="CourseName" name="CourseName" required
                               data-bind="value:course.CourseName" />
                        <span class="k-invalid-msg" data-for="CourseName"></span>
                    </li>
                    <li>
                        <label for="Credit">مبلغ پرداختی</label>
                        <input id="Credit" name="Credit" type="number" min="1000" max="6000"
                               required data-max-msg="عددی بین 1000 و 6000" dir="ltr"
                               data-bind="value:course.Credit"
                               class="k-textbox k-input" />
                        <span class="k-invalid-msg" data-for="Credit"></span>
                    </li>
                    <li>
                        <label for="Email">پست الکترونیک</label>
                        <input type="email" id="Email" dir="ltr" name="Email"
                               data-bind="value:course.Email"
                               required class="k-textbox" />
                    </li>
                    <li>
                        <label for="Tel">تلفن</label>
                        <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}"
                               required class="k-textbox"
                               data-bind="value:course.Tel"
                               data-pattern-msg="8 رقم" />
                    </li>
                    <li>
                        <input type="checkbox" name="Accept"
                               data-bind="checked:accepted"
                               required />
                        شرایط دوره را قبول دارم.
                        <span class="k-invalid-msg" data-for="Accept"></span>
                    </li>
                    <li>
                        <button class="k-button"
                                data-bind="enabled: accepted, click: doSave"
                                type="submit">
                            ارسال
                        </button>
                        <button class="k-button" data-bind="click: resetModel">از نو</button>
                    </li>
                </ul>
                <span id="doneMsg"></span>
            </form>
        </div>
برای اتصال ViewModel تعریف شده به ناحیه‌ی مشخص شده با DIV ایی با Id مساوی coursesSection، می‌توان از متد kendo.bind استفاده کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
            // ...
            });

            var viewModel = kendo.observable({
            // ...
            });

            kendo.bind($("#coursesSection"), viewModel);
        });
    </script>
به این ترتیب Kendo UI به بر اساس تعریف data-bind یک فیلد، برای مثال تغییرات خواص course.UserName را به text box نام کاربر منتقل می‌کند و همچنین اگر کاربر اطلاعاتی را در این text box وارد کند، بلافاصله این تغییرات در خاصیت course.UserName منعکس خواهند شد.
<input type="text" id="UserName" name="UserName" class="k-textbox"
       data-bind="value:course.UserName"
       required />

بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهله‌ای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترل‌های برنامه، تنها کافی است ویژگی‌های data-bind ایی را به آن‌ها اضافه کنیم.
در ناحیه‌ی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شده‌است (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کرده‌ایم:
<input type="checkbox" name="Accept"
       data-bind="checked:accepted"
       required />

<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
برای مثال اگر کاربر این checkbox را انتخاب کند، مقدار خاصیت accepted، مساوی true خواهد شد. تغییر مقدار این خاصیت، توسط ViewModel بلافاصله در کل ناحیه coursesSection منتشر می‌شود. به همین جهت ویژگی enabled: accepted که به معنای مقید بودن فعال یا غیرفعال بودن دکمه بر اساس مقدار خاصیت accepted است، دکمه را فعال می‌کند، یا برعکس و برای انجام این عملیات نیازی نیست کدنویسی خاصی را انجام داد. در اینجا بین checkbox و button یک سیم کشی برقرار است.


ارسال داده‌های تغییر کرده‌ی ViewModel به سرور

تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شده‌اند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المان‌های فرم متصل می‌کند نیز در ادامه اضافه شده‌است. توسط ویژگی‌های data-bind کار Declarative data binding انجام می‌شود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                //...
            });

            var dataSource = new kendo.data.DataSource({
                type: 'json',
                transport: {
                    read: {
                        url: "api/registrations",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    create: {
                        url: "api/registrations",
                        contentType: 'application/json; charset=utf-8',
                        type: "POST"
                    },
                    update: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    parameterMap: function (data, type) {
                        // Convert to a JSON string.  Without this step your content will be form encoded.
                        return JSON.stringify(data);
                    }
                },
                schema: {
                    model: model
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });

            var viewModel = kendo.observable({
                //...
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
در اینجا تعریف DataSource کار با منبع داده راه دور ASP.NET Web API را مشاهده می‌کنید. تعاریف اصلی آن با تعاریف مطرح شده در مطلب «فعال سازی عملیات CRUD در Kendo UI Grid» یکی هستند. هر قسمت آن مانند read، create، update و destory به یکی از متدهای کنترلر ASP.NET Web API اشاره می‌کنند. حالت‌های update و destroy بر اساس Id ردیف انتخابی کار می‌کنند. این Id را باید در قسمت model مربوط به اسکیمای تعریف شده، دقیقا مشخص کرد. عدم تعریف فیلد id، سبب خواهد شد تا عملیات update نیز در حالت create تفسیر شود.


متصل کردن DataSource به ViewModel

تا اینجا DataSource ایی جهت کار با سرور تعریف شده‌است؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
    <script type="text/javascript">
        $(function () {
            $("#coursesSection").kendoValidator({
                // ...
            });

            var model = kendo.data.Model.define({
                // ...
            });

            var dataSource = new kendo.data.DataSource({
                // ...
            });

            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                doSave: function (e) {
                    e.preventDefault();
                    console.log("this", this.course);
                    var validator = $("#coursesSection").data("kendoValidator");
                    if (validator.validate()) {
                        if (this.course.Id == 0) {
                            dataSource.add(this.course);
                        }
                        dataSource.sync(); // push to the server
                        this.set("course", new model()); // reset controls
                    }
                },
                resetModel: function (e) {
                    e.preventDefault();
                    this.set("course", new model());
                }            
             });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
همانطور که در تعاریف تکمیلی viewModel مشاهده می‌کنید، اینبار دو متد جدید دلخواه doSave و resetModel را اضافه کرده‌ایم.
در متد doSave، ابتدا بررسی می‌کنیم آیا اعتبارسنجی فرم با موفقیت انجام شده‌است یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه می‌کنیم. در اینجا بررسی شده‌است که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت می‌گردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شده‌است.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن می‌توان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال می‌کند.
متد دلخواه  resetModel سبب مقدار دهی مجدد شیء course با یک وهله‌ی جدید از شیء model می‌شود. همینقدر برای پاک کردن تمامی کنترل‌های صفحه کافی است.

تا اینجا دو متد جدید را در ViewModel برنامه تعریف کرده‌ایم. در مورد نحوه‌ی اتصال آن‌ها به View، به کدهای دو دکمه‌ی موجود در فرم دقت کنید:
<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
<button class="k-button" data-bind="click: resetModel">از نو</button>
این متدها نیز توسط ویژگی‌های data-bind به هر دکمه نسبت داده شده‌اند. به این ترتیب برای مثال با کلیک کاربر بر روی دکمه‌ی submit، متد doSave موجود در ViewModel فراخوانی می‌شود.


مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات

در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی می‌گردد. نکته‌ی مهمی که در اینجا باید به آن دقت داشت، نحوه‌ی بازگشت Id رکورد جدید ثبت شده‌است.  اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال می‌کند.
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using KendoUI07.Models;

namespace KendoUI07.Controllers
{
    public class RegistrationsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            RegistrationsDataSource.LatestRegistrations.Remove(item);
            return Request.CreateResponse(HttpStatusCode.OK, item);
        }

        public IEnumerable<Registration> Get()
        {
            return RegistrationsDataSource.LatestRegistrations;
        }

        public HttpResponseMessage Post(Registration registration)
        {
            if (!ModelState.IsValid)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            var id = 1;
            var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            registration.Id = id;
            RegistrationsDataSource.LatestRegistrations.Add(registration);

            // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود
            return Request.CreateResponse(HttpStatusCode.Created, registration);
        }

        [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT'
        public HttpResponseMessage Update(int id, Registration registration)
        {
            var item = RegistrationsDataSource.LatestRegistrations
                                        .Select(
                                            (prod, index) =>
                                                new
                                                {
                                                    Item = prod,
                                                    Index = index
                                                })
                                        .FirstOrDefault(x => x.Item.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);


            if (!ModelState.IsValid || id != registration.Id)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            RegistrationsDataSource.LatestRegistrations[item.Index] = registration;
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
در اینجا بیشتر امضای این متدها مهم هستند، تا منطق پیاده سازی شده در آن‌ها. همچنین بازگشت Id رکورد جدید، توسط متد Post نیز بسیار مهم است و سبب می‌شود تا DataSource بداند با فراخوانی متد sync آن، باید عملیات Post یا create انجام شود یا Put و update.


نمایش آنی اطلاعات ثبت شده در یک لیست

ردیف‌های اضافه شده به منبع داده را می‌توان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                           <!--فرم بحث شده در ابتدای مطلب-->
            </form>
        </div>
        <div id="results">
            <table class="metrotable">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>نام</th>
                        <th>دوره</th>
                        <th>هزینه</th>
                        <th>ایمیل</th>
                        <th>تلفن</th>
                        <th></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody>
                <tfoot data-template="footer-template" data-bind="source: this"></tfoot>
            </table>
            <script id="row-template" type="text/x-kendo-template">
                <tr>
                    <td data-bind="text: Id"></td>
                    <td data-bind="text: UserName"></td>
                    <td dir="ltr" data-bind="text: CourseName"></td>
                    <td>
                        #: kendo.toString(get("Credit"), "c0") #
                    </td>
                    <td data-bind="text: Email"></td>
                    <td data-bind="text: Tel"></td>
                    <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td>
                    <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td>
                </tr>
            </script>
            <script id="footer-template" type="text/x-kendo-template">
                <tr>
                    <td colspan="3"></td>
                    <td>
                        جمع کل: #: kendo.toString(totalPrice(), "c0") #
                    </td>
                    <td colspan="2"></td>
                    <td></td>
                    <td></td>
                </tr>
            </script>
        </div>
    </div>
در ناحیه‌ی coursesSection که توسط متد kendo.bind به viewModel برنامه متصل شده‌است، یک جدول را برای نمایش ردیف‌های ثبت شده توسط کاربر اضافه کرده‌ایم. thead آن بیانگر سر ستون جدول است. قسمت tbody و tfoot این جدول توسط دو Kendo UI Template مقدار دهی شد‌ه‌اند. هر کدام نیز منبع داده‌اشان را از view model دریافت می‌کنند. در row-template معادل خواص شیء course را مشاهده می‌کنید. در footer-template متد totalPrice برای نمایش جمع ستون هزینه اضافه شده‌است. بنابراین مطابق این قسمت از View، به یک خاصیت جدید coursesDataSourceRows و سه متد deleteCourse، editCourse و totalPrice نیاز است:
    <script type="text/javascript">
        $(function () {
            // ...
            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                coursesDataSourceRows: new kendo.data.ObservableArray([]),
                doSave: function (e) {
                       // ...
                },
                resetModel: function (e) {
                      // ...
                },
                totalPrice: function () {
                    var sum = 0;
                    $.each(this.get("coursesDataSourceRows"), function (index, item) {
                        sum += item.Credit;
                    });
                    return sum;
                },
                deleteCourse: function (e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    dataSource.remove(course);
                    dataSource.sync(); // push to the server
                },
                editCourse: function(e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    this.set("course", course);
                }
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
نحوه‌ی اتصال خاصیت جدید coursesDataSourceRows که به عنوان منبع داده ردیف‌های row-template عمل می‌کند، به این صورت است:
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه می‌شود تا در ناحیه‌ی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
    <script type="text/javascript">
        $(function () {
            var dataSource = new kendo.data.DataSource({
                //...
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });
        });
    </script>
متد change آن، هر زمانیکه اطلاعاتی در منبع داده تغییر کنند یا اطلاعاتی به سمت سرور ارسال یا دریافت گردد، فراخوانی می‌شود. در همینجا فرصت خواهیم داشت تا خاصیت coursesDataSourceRows را جهت نمایش اطلاعات موجود در منبع داده، مقدار دهی کنیم. همین مقدار دهی ساده سبب اجرای row-template برای تولید ردیف‌های جدول می‌شود. استفاده از new kendo.data.ObservableArray سبب خواهد شد تا اگر اطلاعاتی در فرم برنامه تغییر کند، این اطلاعات بلافاصله در لیست گزارش برنامه نیز منعکس گردد.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
KendoUI07.zip
مطالب
طرحبندی صفحات وب با بوت استرپ 4 - قسمت دوم
امکان ایجاد ستون‌ها‌ی تو در تو در بوت استرپ 4

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

یک مثال: ایجاد ستون‌های تو در تو
<head>
    <style>
        img {
          width: 100%;
          height: 200px;
          max-height: 200px;
        }
      </style>
</head>

<body>
    <div class="container" id="services">
        <div class="row">
            <section class="col-sm-8">
                <img src="images/image.png" alt="sample image">
                <h4>Exotic Pets</h4>
                <p>We offer <strong>specialized</strong> care for <em>reptiles,
                        rodents, birds,</em> and other exotic pets.</p>
            </section>
            <section class="col-sm-4">
                <div class="row no-gutters">
                    <div class="col-2 col-sm-4">
                        <img src="images/image.png" class="img-thumbnail" alt="sample image">
                        <p>Image 1</p>
                    </div>
                    <div class="col-2 col-sm-4">
                        <img src="images/image.png" class="img-thumbnail" alt="sample image">
                        <p>Image 2</p>
                    </div>
                    <div class="col-2 col-sm-4">
                        <img src="images/image.png" class="img-thumbnail" alt="sample image">
                        <p>Image 3</p>
                    </div>
                    <div class="col-2 col-sm-4">
                        <img src="images/image.png" class="img-thumbnail" alt="sample image">
                        <p>Image 4</p>
                    </div>
                    <div class="col-2 col-sm-4">
                        <img src="images/image.png" class="img-thumbnail" alt="sample image">
                        <p>Image 5</p>
                    </div>
                    <div class="col-2 col-sm-4">
                        <img src="images/image.png" class="img-thumbnail" alt="sample image">
                        <p>Image 6</p>
                    </div>
                </div>
            </section>
        </div>
    </div>
</body>
با این خروجی، در اندازه‌ی صفحه‌ی کوچک‌تر از sm:


و با اندازه‌ی صفحه‌ی بزرگ‌تر از sm:


توضیحات:
تعریف گرید تو در تو را در داخل دومین section تعریف شده، در کدهای فوق مشاهده می‌کنید:
<body>
    <div class="container" id="services">
        <div class="row">
            <section class="col-sm-8">
            </section>
            <section class="col-sm-4">
                <div class="row no-gutters">

                </div>
            </section>
        </div>
    </div>
</body>
داخل این section، یک row جدید تعریف شده‌است.
به صورت پیش‌فرض در بین ستون‌ها، یک فاصله‌ی 15px پیش‌فرض وجود دارد که به آن Gutter نیز گفته می‌شود. برای عدم نمایش و اعمال آن می‌توان از کلاس no-gutters استفاده کرد. به همین جهت در تصویر دوم، ستون‌های تعریف شده به هم چسبیده‌اند.
سپس هر ستون داخل این ردیف را به صورت زیر تعریف کرده‌ایم:
<div class="col-2 col-sm-4">
      <img src="images/image.png" class="img-thumbnail" alt="sample image">
      <p>Image 1</p>
</div>
به این معنا که اگر اندازه‌ی صفحه کمتر از sm باشد، تعریف col-2 مورد استفاده قرار می‌گیرد و هر ستون، 2 واحد از 12 واحد را به خود اختصاص می‌دهد. به همین جهت در تصویر اول، تمام تصاویر را در یک سطر مشاهده می‌کنید. پس از گذشتن از این break-point، تنظیم col-sm-4 مورد استفاده قرار می‌گیرد. یعنی هر ستون، 4 واحد از 12 واحد موجود را استفاده می‌کند. در این حالت در هر ردیف، سه تصویر نمایش داده خواهند شد.


امکان تغییر ترتیب نمایش ستون‌ها‌ی گرید بوت استرپ 4

امکان تغییر ترتیب نمایش ستون‌های گرید، در بوت استرپ 4 پیش بینی شده‌است و این مورد نیز بر اساس break-pointهای مختلف، قابل تنظیم است که فرمول کلاس‌های آن‌را در ذیل مشاهده می‌کنید:

در اینجا ذکر break-point اختیاری است و عدد ord بین یک تا 12 تغییر می‌کند.

یک مثال: تغییر ترتیب نمایش ستون‌های گرید
<head>
    <style>
        img {
          width: 100%;
          height: 200px;
          max-height: 200px;
        }
    </style>
</head>

<body>
    <div class="container" id="services">
        <h2>Flex Order</h2>
        <div class="row">
            <section class="col order-2 d-flex flex-column">
                <img src="images/image.png" class="order-2" alt="sample image">
                <h4>1. Exotic Pets</h4>
                <p>We offer <strong>specialized</strong> care for <em>reptiles,
                        rodents, birds,</em> and other exotic pets.</p>
            </section>
            <section class="col order-1">
                <img src="images/image.png" alt="sample image">
                <h4>2. Grooming</h4>
                <p>Our therapeutic <span class="font-weight-bold">grooming</span>
                    treatments help battle fleas, allergic dermatitis, and
                    other challenging skin conditions.</p>
            </section>
            <section class="col order-3">
                <img src="images/image.png" alt="sample image">
                <h4>3. General Health</h4>
                <p>Wellness and senior exams, ultrasound, x-ray, and dental
                    cleanings are just a few of our general health services.</p>
            </section>
        </div>
    </div>
</body>
با این خروجی:


در این مثال توسط کلاس order، مکان ستون‌ها را تغییر داده و اولین ستون را در مکان دوم، دومی را در مکان اول و سومی را در همان مکان خودش نمایش داده‌ایم. باید دقت داشت که در حین تعریف کلاس order بهتر است برای تمام ستون‌ها این ترتیب را تعریف کرد تا با نتایج ناخواسته‌ای مواجه نشویم.
همچنین کلاس order را به سایر المان‌های صفحه نیز می‌توان اعمال کرد. برای مثال در تصویر فوق، در ستون دوم نمایش داده شده، متن در بالا و تصویر در پایین قرار گرفته‌است. اینکار را با تبدیل این ستون به یک flex column با افزودن کلاس‌های d-flex flex-column انجام داده‌ایم. سپس با اعمال کلاس order-2 به تصویر، این تصویر ذیل متن نمایش داده شده‌است.

یکی از کاربردهای تغییر ترتیب نمایش ستون‌ها در دنیای واقعی، افزودن break-point به آن‌ها (مطابق فرمول یاد شده) و سپس نمایش منوها، پیش از محتویات صفحه در اندازه‌های کوچکتر صفحه است. برای مثال اگر در حالت عادی، منوهای کنار صفحه نمایش داده می‌شوند و در ستون سوم قرار گرفته‌اند، شاید بخواهید در اندازه‌ی نمایش موبایل، ترتیب نمایش این منوها بالاتر از متن صفحه باشد و در ابتدا قرارگیرد و نه در ترتیب سوم.


امکان تغییر تراز ستون‌ها‌ی گرید بوت استرپ 4

چون طراحی گرید بوت استرپ 4 مبتنی بر Flexbox است، کلاس‌های قابل توجهی از آن جهت غنی سازی این سیستم طرحبندی قابل استفاده هستند:
- برای تغییر تراز عمودی ستون‌ها، کلاس align-items-ALN را می‌توان به «ردیف‌ها» اعمال کرد. در اینجا ALN یکی از مقادیر start ،center و end را می‌تواند داشته باشد.
- برای تغییر تراز خود ستون‌ها، کلاس align-self-ALN را می‌توان به «ستون‌ها» اعمال کرد. در اینجا نیز ALN یکی از مقادیر start ،center و end را می‌تواند داشته باشد.
- برای تغییر تراز افقی ستون‌ها، کلاس justify-content-ALN را می‌توان به «ردیف‌ها» اعمال کرد. البته ذکر عرض ستون‌ها در این حالت الزامی است. در اینجا ALN یکی از مقادیر start ،center ،around ،between و end را می‌تواند داشته باشد.

مثال: بررسی روش تغییر تراز ستون‌ها
<head>
    <style>
        img {
          width: 100%;
          height: 100px;
          max-height: 100px;
        }
    </style>
</head>

<body>
    <div class="container" id="services">

        <div class="row bg-info align-items-center" style="height: 100vh;">
            <div class="col">
                <div class="row">
                    <section class="col">
                        <img src="images/image.png" alt="sample image">
                        <h4>Exotic Pets</h4>
                        <p>We offer specialized care for reptiles, rodents,
                            birds, and
                            other exotic pets.</p>
                    </section>
                    <section class="col">
                        <img src="images/image.png" alt="sample image">
                        <h4>Grooming</h4>
                        <p>Our therapeutic grooming treatments help battle
                            fleas,
                            allergic dermatitis, and other challenging skin
                            conditions.</p>
                    </section>
                    <section class="col">
                        <img src="images/image.png" alt="sample image">
                        <h4>General Health</h4>
                        <p>Wellness and senior exams, ultrasound, x-ray, and
                            dental
                            cleanings.</p>
                    </section>
                </div>
            </div>
        </div>

        <div class="row bg-success" style="height: 100vh;">
            <section class="col">
                <img src="images/image.png" alt="sample image">
                <h4>Exotic Pets</h4>
                <p>We offer specialized care for reptiles, rodents,
                    birds, and
                    other exotic pets.</p>
            </section>
            <section class="col align-self-center">
                <img src="images/image.png" alt="sample image">
                <h4>Grooming</h4>
                <p>Our therapeutic grooming treatments help battle
                    fleas,
                    allergic dermatitis, and other challenging skin
                    conditions.</p>
            </section>
            <section class="col align-self-end">
                <img src="images/image.png" alt="sample image">
                <h4>General Health</h4>
                <p>Wellness and senior exams, ultrasound, x-ray, and
                    dental
                    cleanings.</p>
            </section>
        </div>

        <div class="row bg-warning justify-content-center" style="height: 100vh;">
            <section class="col-4">
                <img src="images/image.png" alt="sample image">
                <h4>Exotic Pets</h4>
                <p>We offer specialized care for reptiles, rodents, birds, and
                    other exotic pets.</p>
            </section>
            <section class="col-4">
                <img src="images/image.png" alt="sample image">
                <h4>Grooming</h4>
                <p>Our therapeutic grooming treatments help battle fleas,
                    allergic dermatitis, and other challenging skin conditions.</p>
            </section>
        </div>

    </div><!-- container -->
</body>
توضیحات:
در اینجا برای هر ردیف یک height: 100vh درنظر گرفته شده‌است تا کل ارتفاع view-port را پر کند و همچنین برای هر ردیف نیز یک رنگ پس زمینه درنظر گرفته‌ایم تا تغییر ترازها، مشخص‌تر باشند.
ابتدا داخل container چنین تعریفی را مشاهده می‌کنید:
        <div class="row bg-info align-items-center" style="height: 100vh;">
            <div class="col">
                <div class="row">
                    <section class="col">
توسط کلاس align-items-center که به row اعمال شده، سه ستون تعریف شده‌ی آن‌را در میانه‌ی صفحه قرار داده‌ایم.
وجود row و col بعدی که داخل col اصلی تعریف شده‌است، سبب می‌شوند تا تمام آیتم‌ها در یک سطر و در یک تراز افقی نمایش داده شوند. اگر این row و col دوم را حذف کنیم، هر آیتم نسبت به محتوای آن در میانه‌ی صفحه قرار می‌گیرد و یکی بالاتر و دیگری پایین‌تر نمایش داده خواهند شد.


سپس در ردیف بعدی، کلاس‌های align-self-center و align-self-end را بر روی ستون‌ها آزمایش کرده‌ایم:


و در آخر تاثیر اعمال  justify-content-center را بر روی یک ردیف مشاهده می‌کنید:


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


روش‌های دیگری برای تعیین محل قرارگیری ستون‌های بوت استرپ 4

علاوه بر روش‌هایی که تاکنون آن‌ها را بررسی کردیم، کلاس‌های دیگری نیز برای تعیین محل قرارگیری ستون‌های بوت استرپ تدارک دیده شده‌اند:
- کلاس‌های تعیین محل ستون‌ها: fixed-top, fixed-bottom, sticky-top
fixed-top: ستون را در بالای صفحه قرار می‌دهد.
fixed-bottom: ستون و المان را در پایین صفحه قرار می‌دهد.
sticky-top: ستون و المان را در بالای صفحه قرار می‌دهد و با اسکرول صفحه به پایین، باز هم این المان در همان بالای صفحه قابل مشاهده‌است.

- کلاس‌های نمایشی برای شبیه سازی ویژگی‌های CSS:

این کلاس‌ها با d شروع می‌شوند؛ به همراه یک break-point اختیاری که هدف آن‌ها در اختیار گذاشتن توانمندی‌های نمایشی CSS در بوت استرپ است.
برای مثال کلاس d-md-none به این معنا است که پس از رد شدن از اندازه‌ی md، این المان دیگر نمایش داده نخواهد شد.

- کلاس‌های container مقدماتی Flex:

این کلاس‌ها که موارد داخل پرانتز آن‌ها اختیاری است، المان را تبدیل به یک المان Flexbox می‌کنند. حالت نمایشی پیش‌فرض آن‌ها block است؛ اما اگر نیاز بود می‌توان آن‌ها را تبدیل به in-line نیز کرد.

یک مثال: بررسی روش‌های متفاوت تعیین محل قرارگیری المان‌ها

اگر کلاس fixed-bottom را به المانی انتساب دهیم:
    <div class="container bg-success">
        <div class="bg-info fixed-bottom">
            <div class="item">Exotic Pets</div>
            <div class="item">Grooming</div>
            <div class="item">Health</div>
        </div>
آن‌را به طور کامل، از مکان اصلی آن از صفحه خارج کرده و در پایین آن، به صورت ثابت نمایش می‌دهد. در این حالت، این المان حتی با container نیز تراز نیست:


کلاس fixed-top نیز چنین کاری را انجام می‌دهد، فقط المان را بجای پایین صفحه، در بالای صفحه به صورت ثابت نمایش خواهد داد.
در اینجا اگر کلاس sticky-top را اعمال کنیم، هرچند شبیه به fixed-top عمل می‌کند، اما با container تراز است:



تاثیر کلاس‌های flex را در قسمت بعدی به تفصیل بررسی خواهیم کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: Bootstrap4_05.zip
مطالب
آپلود فایل‌ها در یک برنامه‌ی Angular به کمک کامپوننت ng2-file-upload
در مطلب «بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core» روش عمومی آپلود فایل‌ها را بررسی کردیم. آن مطلب وابستگی به کامپوننت خاصی ندارد و عمومی است. در مطلب جاری می‌خواهیم روش دیگری را مبتنی بر کامپوننت ng2-file-upload بررسی کنیم که به همراه نمایش درصد پیشرفت ارسال فایل‌ها، امکان انتخاب بهتر نوع فایل‌های آپلودی و همچنین امکان مشاهده‌ی لیست کامل فایل‌های انتخاب شده و امکان حذف مواردی از آن، پیش از ارسال نهایی است.



پیشنیازهای کار با کامپوننت ng2-file-upload

برای شروع به کار با کامپوننت ng2-file-upload، ابتدا نیاز است بسته‌ی npm آن‌را نصب کرد:
 >npm install ng2-file-upload --save

همچنین یک کامپوننت آزمایشی را هم به برنامه (دقیقا همان مثال مطلب قبلی) جهت اعمال آن اضافه می‌کنیم:
 >ng g c UploadFile/ng2-file-upload-test

پس از آن نیاز است به ماژولی که این کامپوننت جدید در آن قرار دارد، مدخل FileUploadModule کامپوننت ng2-file-upload را افزود:
import { FileUploadModule } from "ng2-file-upload";

@NgModule({
  imports: [
    FileUploadModule
  ]
در غیراینصورت خطای شناخته نشدن خاصیت uploader را در حین اعمال این کامپوننت مشاهده خواهید کرد.


تکمیل Ng2FileUploadTestComponent جهت اعمال ng2-file-upload

اکنون به کلاس کامپوننت جدیدی که ایجاد کردیم، مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
import { FileUploader, FileUploaderOptions } from "ng2-file-upload";
import { Ticket } from "./../ticket";

export class Ng2FileUploadTestComponent implements OnInit {
  fileUploader: FileUploader;
  model = new Ticket();
در اینجا یک خاصیت عمومی از نوع FileUploader تعریف شده‌است که در اختیار قالب این کامپوننت قرار خواهد گرفت. همچنین شیء مدل فرم نیز همانند مطلب «بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core» تهیه شده‌است. هدف این است که بررسی کنیم علاوه بر ارسال فایل‌ها، چگونه می‌توان اطلاعات یک فرم را نیز به سمت سرور ارسال کرد.


وهله سازی از کامپوننت ng2-file-upload و انجام تنظیمات اولیه‌ی آن

پس از تعریف خاصیت عمومی fileUploader، اکنون نوبت به وهله سازی آن است:
    this.fileUploader = new FileUploader(
      <FileUploaderOptions>{
        url: "api/SimpleUpload/SaveTicket",
        headers: [
          { name: "X-XSRF-TOKEN", value: this.getCookie("XSRF-TOKEN") },
          { name: "Accept", value: "application/json" }
        ],
        isHTML5: true,
        // allowedMimeType: ["image/jpeg", "image/png", "application/pdf", "application/msword", "application/zip"]
        allowedFileType: [
          "application",
          "image",
          "video",
          "audio",
          "pdf",
          "compress",
          "doc",
          "xls",
          "ppt"
        ],
        removeAfterUpload: true,
        autoUpload: false,
        maxFileSize: 10 * 1024 * 1024
      }
    );
- در اینجا url، مسیر اکشن متدی را در سمت سرور مشخص می‌کند که قرار است فایل‌های ارسالی را دریافت و ذخیره کند.
- اگر برنامه از نکات anti-forgery token استفاده می‌کند، این کامپوننت برخلاف روش مطرح شده‌ی در مطلب مشابه قبلی، هیچ هدری را به سمت سرور ارسال نمی‌کند. بنابراین نیاز است کوکی مرتبط را خودمان یافته و سپس به لیست هدرها اضافه کنیم. در اینجا روش استخراج یک کوکی را توسط کدهای جاوا اسکریپتی مشاهده می‌کنید:
  getCookie(name: string): string {
    const value = "; " + document.cookie;
    const parts = value.split("; " + name + "=");
    if (parts.length === 2) {
      return decodeURIComponent(parts.pop().split(";").shift());
    }
  }

- برای محدود سازی فایل‌های ارسالی توسط این کامپوننت، دو روش وجود دارد:
الف) مشخص سازی مقدار خاصیت allowedMimeType
همانطور که مشاهده می‌کنید، در اینجا باید mime type فایل‌های مجاز را مشخص کرد.
ب) مشخص سازی مقدار خاصیت allowedFileType
برخلاف تصور، در اینجا از پسوند فایل‌ها استفاده نمی‌کند و از یک لیست از پیش مشخص که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید، کمک گرفته می‌شود. بنابراین اگر برای مثال تنها نیاز به ارسال تصاویر بود، مقدار image را نگه داشته و مابقی را از لیست حذف کنید.

- removeAfterUpload به این معنا است که آیا لیست نهایی که نمایش داده می‌شود، پس از آپلود باقی بماند یا خیر؟
- توسط خاصیت maxFileSize می‌توان حداکثر اندازه‌ی قابل قبول فایل‌های ارسالی را مشخص کرد.


مدیریت رخ‌دادهای کامپوننت ng2-file-upload

اکنون که وهله‌ای از این کامپوننت ساخته شده‌است، می‌توان رخ‌دادهای آن‌را نیز مدیریت کرد. برای مثال:
الف) نحوه‌ی ارسال اطلاعات اضافی به همراه یک فایل به سمت سرور
    this.fileUploader.onBuildItemForm = (fileItem, form) => {
      for (const key in this.model) {
        if (this.model.hasOwnProperty(key)) {
          form.append(key, this.model[key]);
        }
      }
    };
در اینجا شبیه به مطلب مشابه قبلی، مقادیر خواص شیء مدل، به صورت خودکار استخراج شده و به خاصیت form این کامپوننت که درحقیقت همان FormData ارسالی به سمت سرور است، اضافه می‌شوند.

ب) اطلاع یافتن از رخ‌داد خاتمه‌ی کار
رخ‌داد onCompleteAll پس از ارسال تمام فایل‌ها به سمت سرور فراخوانی می‌شود:
    this.fileUploader.onCompleteAll = () => {
      // clear the form
      // this.model = new Ticket();
    };

ج) در حین وهله سازی fileUploader، تعدادی محدودیت نیز قابل اعمال هستند. این محدودیت‌ها سبب نمایش هیچگونه پیام خطایی نمی‌شوند. فقط زمانیکه کاربر فایلی را انتخاب می‌کند، این فایل در لیست ظاهر نمی‌شود. اگر علاقمند به مدیریت این وضعیت باشید، می‌توان از رخ‌داد onWhenAddingFileFailed استفاده کرد:
    this.fileUploader.onWhenAddingFileFailed = (item, filter, options) => {
      // msg: `You can't select ${item.name} file because of the ${filter.name} filter.`
    };

د) اگر ارسال فایلی به سمت سرور با شکست مواجه شود، در ر‌خ‌دادگردان onErrorItem می‌توان به نام این فایل و اطلاعات بیشتری که از سمت سرور دریافت شده‌است، دسترسی یافت:
    this.fileUploader.onErrorItem = (fileItem, response, status, headers) => {
       //
    };

ه) اگر از سمت سرور اطلاعات JSON مانندی یا هر اطلاعات دیگری به سمت کلاینت پس از آپلود ارسال می‌شود، این اطلاعات را می‌توان در رخ‌دادگردان onSuccessItem دریافت کرد:
    this.fileUploader.onSuccessItem = (item, response, status, headers) => {
      if (response) {
        const ticket = JSON.parse(response);
        console.log(`ticket:`, ticket);
      }
    };


ارسال نهایی فرم و  فایل‌ها به سمت سرور

در پایان، با فراخوانی متد uploadAll شیء fileUploader جاری، می‌توان اطلاعات فرم و تمام فایل‌های آن‌را به سمت سرور ارسال کرد:
  submitForm(form: NgForm) {
    this.fileUploader.uploadAll();

    // NOTE: Upload multiple files in one request -> https://github.com/valor-software/ng2-file-upload/issues/671
  }
فقط باید دقت داشت که این کامپوننت هر فایل را جداگانه به سمت سرور ارسال می‌کند و برخلاف روش مطلب قبلی، همه را یکجا و در طی یک درخواست به سمت سرور ارسال نمی‌کند. اما کدهای سمت سرور آن با مطلب مشابه قبلی دقیقا یکی است و تفاوتی نمی‌کند (همان نکات قسمت «دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌» مطلب قبلی نیز در اینجا صادق است).


کدهای کامل کامپوننت ng2-file-upload-test.component.ts را در اینجا می‌توانید مشاهده کنید.


تکمیل قالب کامپوننت Ng2FileUploadTestComponent

اکنون که کار تکمیل کامپوننت آزمایشی ارسال فایل‌ها به سمت سرور به پایان رسید، نوبت به تکمیل قالب آن است.

افزودن فیلد اضافی توضیحات به فرم

<div class="container">
  <h3>Support Form(ng2-file-upload)</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
    <div class="form-group" [class.has-error]="description.invalid && description.touched">
      <label class="control-label">Description</label>
      <input #description="ngModel" required type="text" class="form-control"
        name="description" [(ngModel)]="model.description">
      <div *ngIf="description.invalid && description.touched">
        <div class="alert alert-danger"  *ngIf="description.errors.required">
          description is required.
        </div>
      </div>
    </div>
هدف از این فیلد این است که شیء Ticket را وهله سازی و مقدار دهی کند. از مقدار آن در رخ‌دادگردان onBuildItemForm که پیشتر توضیح داده شد، استفاده می‌شود.

تعریف ویژه‌ی فیلد ارسال فایل‌ها به سمت سرور

    <div class="form-group">
      <label class="control-label">Screenshot(s)</label>
      <input required type="file" multiple ng2FileSelect [uploader]="fileUploader"
        class="form-control" name="screenshot">
    </div>
در اینجا ابتدا دایرکتیو ng2FileSelect ذکر می‌شود تا کامپوننت مرتبط فعالسازی شود. سپس خاصیت uploader این دایرکتیو توسط خاصیت fileUploader که پیشتر در کامپوننت، وهله سازی و تنظیم شد، مقدار دهی می‌شود.
ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده می‌کنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.

نمایش لیست فایل‌ها و نمایش درصد پیشرفت آپلود آن‌ها

جدولی را که در تصویر ابتدای بحث مشاهده کردید، به صورت ذیل شکل می‌گیرد (کدهای آن در همان صفحه‌ی توضیحات کامپوننت نیز موجود هستند):
    <div style="margin-bottom: 10px" *ngIf="fileUploader.queue.length">
      <h3>Upload queue</h3>
      <p>Queue length: {{ fileUploader?.queue?.length }}</p>
      <table class="table">
        <thead>
          <tr>
            <th width="50%">Name</th>
            <th>Size</th>
            <th>Progress</th>
            <th>Status</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let item of fileUploader.queue">
            <td><strong>{{ item?.file?.name }}</strong></td>
            <td nowrap>{{ item?.file?.size/1024/1024 | number:'.2' }} MB</td>
            <td>
              <div class="progress" style="margin-bottom: 0;">
                <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div>
              </div>
            </td>
            <td class="text-center">
              <span *ngIf="item.isError"><i class="glyphicon glyphicon-remove"></i></span>
            </td>
            <td nowrap>
              <button type="button" class="btn btn-danger btn-xs" (click)="item.remove()">
                <span class="glyphicon glyphicon-trash"></span> Remove
              </button>
            </td>
          </tr>
        </tbody>
      </table>

      <div>
        <div>
          Queue progress:
          <div class="progress">
            <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': fileUploader.progress + '%' }"></div>
          </div>
        </div>
        <button type="button" class="btn btn-danger btn-s" (click)="fileUploader.clearQueue()"
          [disabled]="!fileUploader.queue.length">
          <span class="glyphicon glyphicon-trash"></span> Remove all
        </button>
      </div>
    </div>
شیء fileUploader وهله سازی شده‌ی در کامپوننت این قالب، دارای خاصیت queue است. در این خاصیت، لیست فایل‌های انتخابی توسط کاربر درج می‌شوند. برای مثال مقدار fileUploader?.queue?.length مساوی تعداد فایل‌های انتخابی توسط کاربر است. بنابراین می‌توان حلقه‌ای را بر روی آن تشکیل داد و مشخصات این فایل‌ها را در صفحه نمایش داد. همچنین هر آیتم آن دارای متد remove نیز هست. کار این متد، حذف این آیتم از لیست queue است و یا اگر متد fileUploader.clearQueue فراخوانی شود، تمام آیتم‌های این لیست را حذف می‌کند.
در اینجا از progress-bar بوت استرپ برای نمایش درصد آپلود فایل‌ها استفاده شده‌است:
              <div class="progress" style="margin-bottom: 0;">
                <div class="progress-bar" role="progressbar" [ngStyle]="{ 'width': item.progress + '%' }"></div>
              </div>
این کامپوننت ارسال فایل، خاصیت item.progress هر فایل موجود در queue را مدام به روز رسانی می‌کند. به همین جهت می‌توان از آن جهت تغییر عرض پیشرفت progress-bar بوت استرپ استفاده کرد.

غیرفعال کردن دکمه‌ی ارسال، در صورت عدم انتخاب یک فایل

    <button class="btn btn-primary" [disabled]="form.invalid || !fileUploader.queue.length"
      type="submit">Submit</button>
  </form>
اگر بخواهیم انتخاب حداقل یک فایل را توسط کاربر اجباری کنیم، می‌توان خاصیت disabled دکمه‌ی ارسال را به طول صف یا fileUploader.queue.length نیز متصل کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
روش کار با فایل‌های پویای ارائه شده‌ی توسط یک برنامه‌ی ASP.NET Core در برنامه‌های React
پس از آشنایی با «روش کار با فایل‌های ایستا در برنامه‌های React»، اکنون اگر این فایل‌ها ایستا نباشند و توسط یک برنامه‌ی ASP.NET Core بازگشت داده شوند، چطور می‌توان از آن‌ها در برنامه‌های React استفاده کرد؟

برپایی پروژه‌های مورد نیاز

ابتدا یک پوشه‌ی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا می‌کنیم:
> dotnet new react
در مورد این قالب که امکان تجربه‌ی توسعه‌ی یکپارچه‌ی ASP.NET Core و React را میسر می‌کند، در مطلب «روش یکی کردن پروژه‌های React و ASP.NET Core» بیشتر بحث کردیم.
سپس در این پوشه، پوشه‌ی ClientApp پیش‌فرض آن‌را حذف می‌کنیم؛ چون کمی قدیمی است. همچنین فایل‌های کنترلر و سرویس آب و هوای پیش‌فرض آن‌را به همراه پوشه‌ی صفحات Razor آن، حذف می‌کنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
از تنظیم زیر استفاده کرده‌ایم تا با هر بار تغییری در کدهای پروژه‌ی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
بدیهی است در این حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود.

اکنون در ریشه‌ی پروژه‌ی ASP.NET Core ایجاد شده، دستور زیر را صادر می‌کنیم تا پروژه‌ی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
سپس وارد این پوشه‌ی جدید شده و بسته‌های زیر را نصب می‌کنیم:
> cd clientapp
> npm install --save bootstrap axios
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود که برای افزودن فایل bootstrap.css آن به پروژه‌ی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
 import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.
- برای دریافت فایل‌ها از سمت سرور، از کتابخانه‌ی معروف axios استفاده خواهیم کرد.


کدهای سمت سرور دریافت فایل‌های پویا

در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت می‌دهند. این محتوای باینری می‌تواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc;

namespace DownloadFilesSample.Controllers
{
    [Route("api/[controller]")]
    public class ReportsController : Controller
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            return File(virtualPath: "~/app_data/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
فایل بازگشتی فوق که در این مثال در مسیر wwwroot\app_data\sample.pdf برنامه‌ی وب کپی شده‌است، در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.


روش دریافت محتوای باینری در برنامه‌های React

برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایل‌های PDF و اکسل و غیره، مهم‌ترین نکته، تنظیم ویژگی responseType آن به blob است:
  const getResults = async () => {
      const { headers, data } = await axios.get(apiUrl, {
        responseType: "blob"
      });
  }


ساخت URL برای دسترسی به اطلاعات باینری

تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی می‌کنند. این متد، شیء URL را از شیء window جاری دریافت می‌کند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید می‌کند. حاصل آن، یک URL ویژه‌است مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شده‌ی از سرور را به این URL ویژه، مشاهده می‌کنید:
import axios from "axios";
import React, { useEffect, useState } from "react";

export default function DisplayPdf() {
  const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport";

  const [blobInfo, setBlobInfo] = useState({
    blobUrl: "",
    fileName: ""
  });

  useEffect(() => {
    getResults();
  }, []);

  const getResults = async () => {
    try {
      const { headers, data } = await axios.get(apiUrl, {
        responseType: "blob"
      });
      console.log("headers", headers);

      const pdfBlobUrl = window.URL.createObjectURL(data);
      console.log("pdfBlobUrl", pdfBlobUrl);

      const fileName = headers["content-disposition"]
        .split(";")
        .find(n => n.includes("filename="))
        .replace("filename=", "")
        .trim();
      console.log("filename", fileName);

      setBlobInfo({
        blobUrl: pdfBlobUrl,
        fileName: fileName
      });
    } catch (error) {
      console.log(error);
    }
  };
توضیحات:
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شده‌ایم. به این معنا که متد getResults فراخوانی شده‌ی در آن، پس از رندر کامپوننت در DOM فراخوانی می‌شود و بهترین محلی است که از آن می‌توان برای ارسال درخواست‌های Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کرده‌ایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آن‌را تبدیل به یک blob URL کرده‌ایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کرده‌ایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر می‌شود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
 بنابراین می‌توان آن‌را تجزیه کرد و سپس filename را از آن استخراج نمود.
- اکنون که نام فایل و URL دسترسی به داده‌ی فایل باینری دریافتی از سرور را استخراج و ایجاد کرده‌ایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن می‌توان برای مثال فایل PDF دریافتی را نمایش داد.


نمایش فایل PDF دریافتی از سرور، به همراه دکمه‌های دریافت، چاپ و بازکردن آن در برگه‌ای جدید

در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده می‌کنید:
import axios from "axios";
import React, { useEffect, useState } from "react";

export default function DisplayPdf() {

  // ...

  const { blobUrl } = blobInfo;

  return (
    <>
      <h1>Display PDF Files</h1>
      <button className="btn btn-info" onClick={handlePrintPdf}>
        Print PDF
      </button>
      <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}>
        Show PDF in a new tab
      </button>
      <button className="btn btn-success ml-2" onClick={handleDownloadPdf}>
        Download PDF
      </button>

      <section className="card mb-5 mt-3">
        <div className="card-header">
          <h4>using iframe</h4>
        </div>
        <div className="card-body">
          <iframe
            title="PDF Report"
            width="100%"
            height="600"
            src={blobUrl}
            type="application/pdf"
          ></iframe>
        </div>
      </section>

      <section className="card mb-5">
        <div className="card-header">
          <h4>using object</h4>
        </div>
        <div className="card-body">
          <object
            data={blobUrl}
            aria-label="PDF Report"
            type="application/pdf"
            width="100%"
            height="100%"
          ></object>
        </div>
      </section>

      <section className="card mb-5">
        <div className="card-header">
          <h4>using embed</h4>
        </div>
        <div className="card-body">
          <embed
            aria-label="PDF Report"
            src={blobUrl}
            type="application/pdf"
            width="100%"
            height="100%"
          ></embed>
        </div>
      </section>
    </>
  );
}
که چنین خروجی را ایجاد می‌کند:


در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، می‌توان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیت‌های توکار مرورگر صورت می‌گیرد و نیاز به نصب افزونه‌ی خاصی را ندارد.

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


مدیریت دکمه‌ی چاپ PDF

پس از اینکه به blobUrl دسترسی یافتیم، اکنون می‌توان یک iframe مخفی را ایجاد کرد، سپس src آن‌را به این آدرس ویژه تنظیم نمود و در آخر متد print آن‌را فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر می‌شود:
  const handlePrintPdf = () => {
    const { blobUrl } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = blobUrl;
    document.body.appendChild(iframe);
    if (iframe.contentWindow) {
      iframe.contentWindow.print();
    }
  };


مدیریت دکمه‌ی نمایش فایل PDF در یک برگه‌ی جدید

اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگه‌ای جدید نمایش دهید، می‌توان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => {
    const { blobUrl } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    window.open(blobUrl);
  };

مدیریت دکمه‌ی دریافت فایل PDF

بجای نمایش فایل PDF می‌توان دکمه‌ای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
  const handleDownloadPdf = () => {
    const { blobUrl, fileName } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = blobUrl;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
  };
در اینجا یک anchor جدید به صورت مخفی به صفحه اضافه می‌شود که href آن به blobUrl تنظیم شده‌است و همچنین از فایل fileName استخراجی نیز در اینجا جهت ارائه‌ی نام اصلی فایل دریافتی از سرور، کمک گرفته شده‌است. سپس متد click آن فراخوانی خواهد شد. این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایل‌های PDF نیست.
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، می‌توان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگی‌های سمت کلاینت نیز می‌شود، ابتدا به پوشه‌ی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار می‌کند. سپس به پوشه‌ی اصلی برنامه‌ی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائه‌ی برنامه‌ی React بر روی پورت 5001 می‌شود.
مطالب
کتابخانه GMap.Net
نقشه گوگل در حال حاضر یکی از محبوب‌ترین و کاملترین نقشه‌های جهان است و امکانات خوبی هم دارد. در این راستا بسیاری از مردم سعی در استفاده از این نقشه‌ها و امکانات آن‌ها دارند. به همین دلیل گوگل در بسته‌های api خود نیز این مورد را گنجانده است. ولی استفاده از این api مستلزم نوشتن کدهای جاوا اسکرپیتی و شناخت توابع و ثابت‌های api گوگل است. اما در هر صورت این مستندات مورد مطالعه قرار می‌گیرند.

سال گذشته بود که به بررسی کتابخانه‌های موجود برای دات نت که به ساخت نقشه‌های گوگل (+ ) می‌پردازند پرداختم. ولی مشکلی که وجود داشت، همه آن‌ها در نهایت یک تصویر jpeg تحویل می‌دادند. ولی من می‌خواستم نقشه‌ی من زنده و واکنشگرا باشد تا کاربر بتواند روی آن حرکت کند، زوم کند، مارکر‌ها را جابجا کند و امکانات دیگری که در این نقشه در دسترس است را داشته باشد. برای همین شروع به ساخت یک class library کردم تا کاربر بتواند در محیط سی شارپ، تنظیمات را با اسامی قابل شناخت و یک intellisense قدرتمند بنویسد و در نهایت بر اساس اطلاعات کاربر، این کدها به صورت جاوا اسکریپت تولید شود. می‌توانید سورس نهایی کتابخانه‌ی GMap.Net را در گیت هاب، به همراه یک پروژه‌ی نمونه ببینید.

پروژه‌ی وابسته این کتابخانه،  MS Ajax Minifier جهت کم حجم کردن کدهای جاوا اسکریپت است. در مورد این کتابخانه در سایت جاری بحث شده است.
برای نصب این کتابخانه می‌توانید از طریق دستور زیر در Nuget عمل کنید:
Install-Package GMap.Net

در این کتابخانه مواردی که مورد توجه قرار گرفته است، تنظیمات نقشه و بعد از آن overlay‌ها هستند که شامل مارکرها و اشکال مختلف می‌باشند. این اشکال شامل رسم مستطیل بر روی نقشه، چند ضلعی‌ها و ... نیز می‌شوند.

برای شروع نیاز است که یک نمونه از کلاس GoogleMapApi را ایجاد کنید. بعد از آن با استفاده از خصوصیت SetLocation، مختصات مرکز نقشه را تنظیم نمایید. سپس با استفاده از خصوصیات دیگر نیز می‌توانید نقشه را تنظیم نمایید. تعدادی از این خصوصیات مثل SetZoomVisibility هستند که با استفاده از آن می‌توانید تنظیمات زوم را روی نقشه پیاده سازی کنید. البته فعال کردن این گزینه به تنهایی کافی نیست و باید از طریق خصوصیت ZoomControlOption پیکربندی کنترل زوم را نیز اینجام دهید که این پیکربندی شامل موقعیت قرارگیری کنترل‌های زوم و اندازه‌ی دکمه‌ها می‌باشد و مابقی تنظیمات هم بدین شکل هستند:

به غیر از تنظیمات نقشه، Overlayهای زیر در این کلاس پشتیبانی می‌شوند:
عنوان
توضیحات
 Marker  یک نشانه گذار که برای مشخص کردن یک محل بر روی نقشه به کار می‌رود. این علامت گذار شامل خصوصیت‌هایی چون نقطه‌ی قرارگیری، آیکن، عنوان و انیمیشنی برای نحوه‌ی نمایش آن می‌باشد. همچنین شامل یک خصوصیت دیگر از نوع InfoWindow است که به شما امکان نمایش یک پنجره‌ی توضیحات را نیز بر روی مارکر می‌دهد. این محتوا می‌تواند به صورت HTML نمایش یابد.
 Circle  در صورتیکه بخواهید ناحیه‌ای دایره‌ای شکل را بر روی نقشه مشخص کنید، کاربرد دارد. با دادن نقطه‌ی مرکزی و شعاع می‌توانید دایره را ترسیم کنید. همچنین شامل خصوصیات ظاهری چون رنگ داخل و حاشیه‌ها و میزان شفافیت نیز می‌باشد.
 Rectangle  به رسم یک مستطیل می‌پردازد و تنها لازم است دو مختصات را به آن بدهید و بر اساس این دو نقطه، ناحیه‌ی مستطیلی شکل ترسیم می‌گردد. در صورتیکه نقاط بیشتری را به آن اضافه کنید، فقط دوتای اولی در نظر گرفته می‌شوند. این گزینه شامل خصوصیات ظاهری نیز می‌گردد.
 Polyline  برای ترسیم مسیرها به صورت چند ضلعی به کار می‌رود و الزامی به بستن مسیرها نیست. دارای خصوصیات ظاهری نیز می‌باشد.
 
polygon
کاملا شبیه Ployline است؛ با این تفاوت که یک چند ضلعی بسته است و می‌تواند داخل آن با رنگ پر باشد. برای بستن این چند ضلعی لازم نیست تا کاری انجام دهید. خود کلاس، نقطه‌ی اول و آخر را به هم وصل می‌کند.

خصوصیات آیتم‌های بالا، شامل موارد زیر می‌شود:

 نام خصوصیت
توضیحات
 Id  در سازنده‌ی هر کدام به طور اجباری قرار گرفته است. این id برای زمانی است که بخواهید با استفاده از جاوااسکرپیت با آن ارتباط برقرار کنید.
 Editable  با فعال کردن این خاصیت، به کاربر این اجازه را می‌دهید که بتواند روی Overlay ویرایش انجام دهد.
 StrokeWeight  ضخامت لبه‌ها را مشخص می‌کند.
 StrokeColor  رنگ لبه را مشخص می‌کند.
 StrokeOpacity  میزان شفافیت لبه را بین 0 تا 1 مشخص می‌کند.
 FillColor  بعضی از المان‌ها مانند چند ضلعی‌های بسته و مستطیل که ناحیه‌ی داخلی دارند، شامل این گزینه هستند و رنگ داخل این ناحیه را مشخص می‌کنند.
 FillOpacity  میزان شفافیت خصوصیت بالا را از 0 تا 1 مشخص می‌کند.
 Points  با استفاده از این خاصیت می‌توانید مختصات را با استفاده از کلاس Location به آن اضافه کنید. برای دایره خصوصیت Point وجود دارد.
 Radius  برای دایره کاربرد دارد. با مقدار نوع Int می‌توانید شعاع آن را مقدار دهی کنید.
کد زیر و تصویر زیر نمونه‌ای از کاربرد این کلاس است:
public class MiladTower
    {
        public GoogleMapApi TestMarker()
        {
            var map=new GoogleMapApi(true);
            var location = new Location(35.7448416, 51.3753212);
            map.SetLocation(location);
            map.SetZoom(17);
            map.SetMapType(MapTypes.ROADMAP);
            map.SetBackgroundColor(Color.Aqua);
            map.ZoomControlVisibilty(true);
            map.ZoomOptions = new zoomControlOptions()
            {
                Position = Position.TOP_LEFT,
                ZoomStyle = ZoomStyle.SMALL
            };
  

            Marker marker=new Marker("mymarker1");
            marker.InfoWindow=new InfoWindow("iw1")
            {
                Content = "<b>Milad Tower</b><i>in Tehran</i><br/>Milad Tower is the highest tower in iran,many people and tourists visit it each year, but it's so expensive that i cant afford it as iranian citizen<br/>please see more info at  <a href=\"https://en.wikipedia.org/wiki/Milad_Tower\"><img width='16px' height='16px' src='https://en.wikipedia.org/favicon.ico'/>wikipedia</a>"
            };
            marker.MarkerPoint = location;
            map.Markers.Add(marker);

            var circle=new CircleMarker("mymarker2");
            circle.FillColor = Color.Green;
            circle.FillOpacity = 0.6f;
            circle.StrokeColor = Color.Red;
            circle.StrokeOpacity = 0.8f;
            circle.Point = location;
            circle.Radius = 30;
            circle.Editable = true;
            circle.StrokeWeight = 3;
            map.Circles.Add(circle);

            Rectangle rect=new Rectangle("rect1");
            rect.FillColor = Color.Black;
            rect.FillOpacity = 0.4f;
            rect.Points.Add(new Location(35.74728723483808, 51.37550354003906));
            rect.Points.Add(new Location(35.74668641224311, 51.376715898513794));
            map.Rectangles.Add(rect);

            Polyline polyline=new Polyline("poly1");
            polyline.Points.Add(new Location(35.74457043569041, 51.373915672302246));
            polyline.Points.Add(new Location(35.74470976097927, 51.37359380722046));
            polyline.Points.Add(new Location(35.744378863020074, 51.37337923049927));
            polyline.StrokeColor = Color.Blue;
            polyline.StrokeWeight = 2;
            map.Polylines.Add(polyline);

            Polygon polygon=new Polygon("poly2");
            polygon.Points.Add(new Location(35.746494844665094, 51.374655961990356));
            polygon.Points.Add(new Location(35.74635552250061, 51.37283205986023));
            polygon.Points.Add(new Location(35.74598109297522, 51.372681856155396));
            polygon.Points.Add(new Location(35.7454934611854, 51.37361526489258));
            polygon.FillColor = Color.Black;
            polygon.FillOpacity = 0.5f;
            polygon.StrokeColor = Color.Gray;
            polygon.StrokeWeight = 1;
            map.Polygons.Add(polygon);

            return map;
        }
    }
بعد از ایجاد چنین کلاسی نیاز است تا آن را در ویوو ترسیم کنیم. ابتدا یک div برای ناحیه‌ی نقشه ایجاد می‌کنیم و سپس خروجی متد ShowMapForMVC را در ناحیه‌ی Head صفحه چاپ می‌کنیم. این خروجی به طور خودکار یک MVCHTMLString را بر می‌گرداند. در صورتیکه از وب فرم استفاده می‌کنید، می‌توانید از گزینه‌ی ShowMap استفاده کنید.
@section javascript
{
    @{
        var map = new MiladTower().TestMarker();
        @map.ShowMapForMvc("mapdiv")
    }
}
<br/><br/>
<div id="mapdiv" style="width:600px;height:600px;"></div>

در نهایت نقشه‌ی زیر نمایش داده می‌شود:


کم حجم کردن کدها
در صورتیکه به سورس صفحه نگاهی بیندازید، می‌بینید که کدهای جاوا اسکریپت، داخل صفحه نوشته شده‌اند. اگر بخواهید برای کم حجم‌تر شدن کد، عملیات minify را انجام دهید، با true شدن خصوصیت minified با استفاده از کتابخانه‌ی وابسته‌اش (MS Ajax Minifier)  اینکار را انجام می‌دهد.

انتقال کدها به یک فایل خارجی
بسیاری از ما برای نوشتن کدهای جاوا اسکریپت، از یک فایل خارجی استفاده می‌کنیم. برای داشتن این قابلیت می‌توانید به جای ShowMapForMVC متد CallJs را صدا بزنید تا کتابخانه api گوگل را صدا بزند و سپس در یک اکشن متد، متد GiveJustJS را صدا بزنید و طبق مقاله‌ی موجود در سایت جاری محتوای آن را برگردانید و به عنوان یک فایل JS به این اکشن متد لینک بدهید. کدهای زیر به شما نحوه‌ی این کار را نشان می‌دهند:
ابتدا در یک اکشن متد، کد زیر را وارد می‌کنیم:
    public ActionResult MiladJs()
        {
            var output = new MiladTower().TestMarker().GiveJustJs("mapdiv");
            Response.ContentType = "text/javascript";
            return Content(output);
        }

بعد از آن در ویووی مربوطه کد زیر را داریم:
@section javascript
{
    @{
        var map = new MiladTower().TestMarker();
        @map.CallJs()
        <script type="text/javascript" src="@Url.Action("MiladJs","Home")"></script>
    }   
}
<br/><br/>
<div id="mapdiv" style="width:600px;height:600px;"></div>
بدین ترتیب کدهای شما داخل یک فایل خارجی قرار می‌گیرند.


نحوه‌ی کارکرد این کتابخانه
برای آشنایی با نحوه‌ی کارکرد آن باید بدانید که اساس کار این کتابخانه string interpolation است. این کتابخانه کلاسی را به صورت Partial دارد که بین چندین فایل تقسیم شده است و هر یک از فایل‌ها، با نام محتوای آن نامگذاری شده‌اند. Public methods متدهای عمومی، private methods متدهای خصوصی، Constants یا ثابت‌ها که حاوی تمام دستورات جاوا اسکریپتی هستند و در نهایت خود کلاس اصلی GoogleMapApi که حاوی کدهای اجرایی و تشکیل کد جاوا اسکریپت می‌باشد. در کنار کلاس اصلی، کلاس‌های Overlay هم قرار دارند که شامل اطلاعات اشیاء روی نقشه‌ها هستند؛ مثل مارکرها و چندضلعی‌ها و ... و در نهایت یک سری کلاس و نوع شمارشی Enum شامل خصوصیت‌هایی که برای تنظیمات و پیکربندی نقشه به کار می‌روند.
کلاس GoogleMapApi برای ایجاد کدها، داده‌هایی را که برای هر کلاس در نظر گرفته‌اید، با استفاده از interpolation و ثابت‌های حاوی کد جاوا اسکریپت، در یک رشته‌ی جدید قرار می‌دهند و این رشته‌ها با اتصال درست در موقعیت خود، کد نهایی جاوا اسکریپت را تولید می‌کنند.
مطالب
کار با SignalR Core از طریق یک کلاینت جاوا اسکریپتی
کلاینت جاوا اسکریپتی SignalR Core، بازنویسی کامل شده‌است و دیگر وابستگی به jQuery ندارد. این کلاینت از طریق npm توزیع می‌شود:
 npm install @aspnet/signalr-client --save
فایل‌های آن نیز شامل فایل‌های جاوا اسکریپتی مرتبط و همچنین Typings مورد استفاده‌ی در TypeScript است که نمونه‌ای از نحوه‌ی استفاده از این Typings را در مطلب «کار با SignalR Core از طریق یک کلاینت Angular» مطالعه کردید.


بررسی محتوای پوشه‌ی node_modules\@aspnet\signalr-client

پس از نصب بسته‌ی «aspnet/signalr-client@»، در مسیر node_modules\@aspnet\signalr-client\dist دو پوشه‌ی src و browser را خواهید یافت. پوشه‌ی src حاوی منبع کامل این کلاینت و همچنین فایل‌های Typings مخصوص تایپ‌اسکریپت است.


و پوشه‌ی browser آن شامل دو گروه فایل است:


- در اینجا گروهی از فایل‌ها، حاوی عبارت ES5 هستند و تعدادی خیر. SignalR JavaScript بر اساس ES 6 یا EcmaScript 2015 تهیه شده‌است و از مفاهیمی مانند Promises  و  arrow functions استفاده می‌کند. باید دقت داشت که تعدادی از مرورگرها مانند IE از این قابلیت‌ها پیشتیبانی نمی‌کنند. در بین این فایل‌ها، آن‌هایی که حاوی عبارت ES5 نیستند، یعنی بر اساس ES 6 تهیه شده‌اند. سایر فایل‌ها توسط قابلیت Transpile مربوط به TypeScript به ES5 ترجمه شده‌اند. به علاوه حجم این فایل‌ها نیز بیشتر می‌باشد؛ چون حاوی تعاریف وابستگی‌هایی هستند که در ES 5 وجود خارجی ندارند. بنابراین بسته به نوع مرورگر مدنظر، یکی از این دو گروه را باید انتخاب کرد؛ ES 6 برای مرورگرهای جدید و ES 5 برای مرورگرهای قدیمی.
- به علاوه در اینجا تعدادی از فایل‌ها حاوی عبارت msgpackprotocol هستند. نگارش جدید SignalR از پروتکل‌های هاب سفارشی مانند پروتکل‌های باینری نیز پشتیبانی می‌کند. همچنین حاوی یک پیاده سازی توکار از پروتکل‌های باینری بر اساس MessagePack نیز هست. چون حجم کدهای پشتیبانی کننده‌ی از این پروتکل ویژه بالا است، آن‌را به یک فایل مجزا انتقال داده‌اند تا در صورت نیاز مورد استفاده قرار گیرد. بنابراین اگر از این پروتکل استفاده نمی‌کنید، نیازی هم به الحاق آن در صفحات خود نخواهید داشت. فایل third-party-notices.txt نیز مربوط است به یادآوری مجوز استفاده‌ی از MessagePack که MIT می‌باشد.
- در هر گروه نیز، دو فایل min و معمولی قابل مشاهده‌است. فایل‌های min برای توزیع نهایی مناسب هستند و فایل‌های غیرفشرده شده برای حالت دیباگ.


استفاده از کلاینت جاوا اسکریپتی SignalR Core

برای کار با کلاینت جاوا اسکریپتی SignalR Core از همان فایل‌های موجود در پوشه‌ی node_modules/@aspnet/signalr-client/dist/browser استفاده می‌کنیم. تفاوت این کلاینت با نگارش قبلی SignalR به صورت یک ذیل است:
1) ارجاع به فایل قدیمی signalR-2.2.1.min.js با فایل جدید signalR-client-1.0.0-alpha1.js جایگزین می‌شود. اگر می‌خواهید مرورگرهای قدیمی را پشتیبانی کنید، نگارش ES5 آن‌را لحاظ کنید.
2) پروکسی‌ها با new HubConnection جایگزین شده‌اند.
3) برای ثبت callbackهای سمت کلاینت، از متد جدید on استفاده می‌شود.
4) بجای متد done مربوط به jQuery، در اینجا از متد then مربوط به ES6 کمک گرفته شده‌است.
5) کار فراخوانی متدهای هاب توسط متد invoke انجام می‌شود.


یک مثال: بازنویسی قسمت سمت کلاینت مثال «کار با  SignalR Core از طریق یک کلاینت Angular» با jQuery

هرچند کلاینت جدید SignalR Core وابستگی به jQuery ندارد، اما جهت سهولت کار با DOM، کدهای سمت کلاینت مثال قبلی را با jQuery بازنویسی می‌کنیم. تمام کدهای سمت سرور این مثال با مطلب «کار با SignalR Core از طریق یک کلاینت Angular» یکی است؛ مانند ایجاد هاب، فعالسازی SiganlR در فایل آغازین برنامه و ثبت مسیرهاب. بنابراین در اینجا، این قسمت از کدهای سمت سرور را مجددا تکرار نمی‌کنیم و تمام نکات آن یکی هستند.

برای کار با کلاینت جاوا اسکریپتی SignalR Core، اینبار دستور ذیل را در ریشه‌ی پروژه‌ی وب اجرا می‌کنیم (یا هر پروژه‌ای که قرار است مدیریت فایل‌های سمت کلاینت و Viewهای برنامه را انجام دهد):
npm init
npm install @aspnet/signalr-client --save
bower install
دستور اول یک فایل package.json خالی را ایجاد می‌کند و دستور دوم بسته‌ی جاوا اسکریپتی SiganlR Core را نصب خواهد کرد. به علاوه این وابستگی را در فایل package.json نیز ثبت می‌کند. دستور سوم نیز وابستگی‌های قید شده‌ی در فایل bower.json را نصب می‌کند.

مرحله‌ی بعدی کار، تنظیمات فایل bundleconfig.json است؛ تا تمام اسکریپت‌های مورد نیاز جمع‌آوری و یکی شوند:
[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/lib/bootstrap/dist/css/bootstrap.min.css",
      "wwwroot/css/site.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/lib/jquery/dist/jquery.min.js",
      "wwwroot/lib/bootstrap/dist/js/bootstrap.min.js",
      "node_modules/@aspnet/signalr-client/dist/browser/signalr-client-1.0.0-alpha1-final.min.js",
      "wwwroot/lib/jquery-validation/dist/jquery.validate.min.js",
      "wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
      "wwwroot/lib/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js",
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": false,
      "renameLocals": false
    },
    "sourceMap": false
  }
]
در اینجا نحوه‌ی ثبت فایل signalr-client-1.0.0-alpha1-final.min.js مبتنی بر ES 6 را مشاهده می‌کنید. اگر می‌خواهید نگارش ES 5 آن‌را ذکر کنید، از فایل signalr-clientES5-1.0.0-alpha1-final.min.js استفاده نمائید.
با توجه به خروجی‌های نهایی فایل bundleconfig.json، تنها نیاز است مداخل ذیل را به فایل layout برنامه اضافه کرد:
<link href="~/css/site.min.css" rel="stylesheet" asp-append-version="true" />
<script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>

مرحله‌ی بعد، تغییر نام متد send قسمت قبل به broadcastMessage است:
public class MessageHub : Hub
{
   public Task Send(string message)
   {
     return Clients.All.InvokeAsync("broadcastMessage", message);
   }
}
به این ترتیب می‌توان به تمایز بهتری بین نام callback سمت کلاینت و متد Send سمت سرور رسید. بهتر است این‌دو هم‌نام نباشند.
در ادامه یک کنترلر ساده را به نام JsClientController با View ذیل ایجاد می‌کنیم:
<form method="post"
      asp-action="Index"
      asp-controller="Home"
      data-ajax="true"
      role="form">
  <div class="form-group">
     <label label-for="message">Message: </label>
     <input id="message" name="message" class="form-control"/>
  </div>
  <button class="btn btn-primary" type="submit">Send To Home/Index</button>
  <button class="btn btn-success" id="sendmessageDirect" type="button">Send To /message hub directly</button>
</form>

<div id="discussion">
</div>
کار آن نمایش فرم ذیل است:


از اولین دکمه برای ارسال یک پیام به کنترلر Home که در آن توسط <IHubContext<MessageHub پیامی به تمام کلاینت‌ها ارسال می‌شود، استفاده شده‌است. دومین دکمه متد Send هاب را مستقیما فراخوانی می‌کند؛ با این کدهای سمت کلاینت:
@section Scripts
{
   <script type="text/javascript" asp-append-version="true">
   $(function() {
      var connection = new signalR.HubConnection('/message');
      connection.on('broadcastMessage', function (message) {
          // Add the message to the page.
          var encodedMsg = $('<div />').text(message).html();
          $('#discussion').append('<li>' + encodedMsg + '</li>');
      });

      connection.start().then(function () {
          console.log('connected.');
          $('#sendmessageDirect').click(function () {
              // Call the Send method on the hub.
              connection.invoke('send', $('#message').val());
          });
      });
   });
   </script>
}
- ابتدا یک شیء جدید signalR.HubConnection ایجاد می‌شود. این شیء به آدرس Hub تعریف شده‌ی در فایل آغازین برنامه اشاره می‌کند.
- سپس در متد on هست که مشخص می‌کنیم متد سمت کلاینتی که قرار است از سمت سرور فراخوانی شود، چه نامی دارد. نام آن‌را در این مثال broadcastMessage درنظر گرفته‌ایم. در اینجا پارامتر message از سمت سرور دریافت شده و سپس در صفحه‌ی جاری نمایش داده می‌شود.
بدیهی است متد Send می‌تواند تعداد پارامترهای بیشتری را بپذیرد و همچنین متد broadcastMessage نیز محدودیتی از لحاظ تعداد پارامتر ندارد. اگر پارامترهای بیشتری را تعریف کردید، در همینجا باید قید شوند.
- در ادامه کار شروع این اتصال آغاز می‌شود. در متد then هست که باید کار اتصال دکمه‌ی sendmessageDirect صورت گیرد. چون عملیات اتصال ممکن است زمانبر باشد و connection ارسالی هنوز آغاز نشده باشد. در اینجا نحوه‌ی فراخوانی مستقیم متد Send سمت سرور را با یک پارامتر ملاحظه می‌کنید. این متد نیز می‌تواند بر اساس امضای متد Send سمت سرور، تعداد پارامترهای بیشتری را قبول کند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: SignalRCore2WebApp02.zip
برای اجرا آن باید این دستورات را به ترتیب وارد کنید:
dotnet restore
npm install
npm install -g bower
bower install
dotnet watch run
مطالب
مسیریابی در Angular - قسمت هشتم - مسیرهای ثانویه
به چندین مسیر که در یک زمان و در یک سطح، نمایش داده می‌شوند، مسیرهای ثانویه (secondary routes) گفته می‌شوند و برای ساخت رابط‌های کاربری پیچیده مفید هستند. از آن‌ها می‌توان برای نمایش چندین پنل در یک صفحه استفاده کرد که هر کدام دارای محتوایی متفاوت، به همراه مسیریابی مستقل و خاص خودشان هستند؛ مانند ساخت یک صفحه‌ی مدیریتی. هرچند می‌توان این صفحه‌ی مدیریتی را با درج مستقیم کامپوننت‌های آن‌ها در یک صفحه نیز نمایش داد، اما اگر هر کدام نیاز به مسیریابی خاصی نیز جهت نمایش جزئیات آن‌ها داشته باشند، دیگر روش درج مستقیم کامپوننت‌ها توسط selector آ‌ن‌ها در صفحه پاسخگو نخواهد بود.


 مروری بر نحوه‌ی کارکرد مسیریابی اصلی برنامه

 به router-outlet ایی که در فایل قالب src\app\app.component.html قرار گرفته‌است، primary outlet می‌گویند. زمانیکه کاربر، برنامه را در مرورگر مشاهده می‌کند، با هربار کلیک بر روی یکی از لینک‌های منوی بالای سایت، قالب آن‌را در این primary outlet مشاهده می‌کند. اگر بخواهیم پنل دیگری را در همین صفحه و در همین سطح از نمایش، درج کنیم، نیاز به تعریف outlet دیگری است که به همراه مسیرهای ثانویه‌ای نیز خواهد بود.


تعریف یک router-outlet نامدار

با توجه به اینکه هر پنل به همراه مسیریابی ثانویه، نیاز به router-outlet خودش را خواهد داشت، مسیریاب برای اینکه بداند محتوای آن‌ها را در کجای صفحه درج کند، به نام‌های آن‌ها مراجعه می‌کند. به این ترتیب می‌توان چندین router-outlet را در یک سطح از نمایش تعریف کرد؛ اما هرکدام باید دارای نامی منحصربفرد باشند.
در مثال این سری می‌خواهیم پنلی را در سمت راست صفحه‌ی اصلی درج کنیم. برای تعریف آن در همان سطحی که router-outlet اصلی قرار دارد، نیاز است فایل src\app\app.component.html را ویرایش کنیم:
<div class="container">
  <div class="row">
    <div class="col-md-10">
      <router-outlet></router-outlet>
    </div>
    <div class="col-md-2">
      <router-outlet name="popup"></router-outlet>
    </div>
  </div>
</div>
در اینجا با استفاده از امکانات بوت استرپ، دو ستون را در قالب اصلی برنامه تعریف کرده‌ایم. ستون اول حاوی router-outlet اصلی برنامه است و ستون دوم جهت درج پنل پیام‌های برنامه تعریف شده‌است. این router-outlet دوم، با نام popup مشخص گردیده‌است.


افزودن ماژول جدید پیام‌های سیستم

در ادامه ماژول جدید پیام‌های سیستم را به همراه تنظیمات ابتدایی مسیریابی آن اضافه خواهیم کرد که در آن ماژول، مدیریت نمایش پیام‌های مختلفی در router-outlet ثانویه popup صورت خواهد گرفت:
 >ng g m message --routing
به این ترتیب دو فایل src\app\message\message-routing.module.ts و src\app\message\message.module.ts به برنامه اضافه می‌شوند.

در ادامه نیاز است MessageModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم (پیش از AppRoutingModule که حاوی مسیریابی catch all است):

import { MessageModule } from './message/message.module';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),

    ProductModule,
    UserModule,
    MessageModule,

    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

سپس کامپوننت جدید Message را به ماژول Message برنامه اضافه می‌کنیم:
 >ng g c message/message
که اینکار سبب به روز رسانی فایل message.module.ts جهت تکمیل قسمت declarations آن با MessageComponent نیز می‌شود.

پس از آن یک سرویس ابتدایی پیام‌های کاربران را نیز اضافه خواهیم کرد:
 >ng g s message/message -m message/message.module
که سبب افزوده شدن سرویس message.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول message.module.ts نیز می‌شود:
 installing service
  create src\app\message\message.service.spec.ts
  create src\app\message\message.service.ts
  update src\app\message\message.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل message.service.ts آن‌را به نحو ذیل تکمیل می‌کنیم:
import { Injectable } from '@angular/core';

@Injectable()
export class MessageService {
  private messages: string[] = [];
  isDisplayed = false;

  addMessage(message: string): void {
    let currentDate = new Date();
    this.messages.unshift(message + ' at ' + currentDate.toLocaleString());
  }
}
هدف از این سرویس، به اشتراک گذاری اطلاعات بین کامپوننت‌های مختلف برنامه است. هر قسمت از برنامه (هر کامپوننتی) می‌تواند این سرویس را در سازنده‌ی خود تزریق کرده و پیامی را به مجموعه‌ی پیام‌های موجود اضافه کند.

اکنون جهت تکمیل کامپوننت پیام‌ها، ابتدا فایل قالب message.component.html را به نحو ذیل تکمیل می‌کنیم:
<div class="row">
  <h4 class="col-md-10">Message Log</h4>
  <span class="col-md-2">
      <a class="btn btn-default"  (click)="close()">x</a>
   </span>
</div>
<div *ngFor="let message of messageService.messages; let i=index">
  <div *ngIf="i<10" class="message-row">
    {{ message }}
  </div>
</div>
به این ترتیب تنها 10 پیام از مجموعه پیام‌های سرویس پیام‌ها، توسط قالب این کامپوننت نمایش داده خواهد شد. یک دکمه‌ی بستن نیز در اینجا اضافه شده‌است.
کدهای کامپوننت این قالب به صورت ذیل است:
import { MessageService } from './../message.service';
import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';

@Component({
  //selector: 'app-message',
  templateUrl: './message.component.html',
  styleUrls: ['./message.component.css']
})
export class MessageComponent implements OnInit {

  constructor(private messageService: MessageService,
    private router: Router) { }

  ngOnInit() {
  }

  close(): void {
    // Close the popup.
    this.router.navigate([{ outlets: { popup: null } }]);
    this.messageService.isDisplayed = false;
  }
}
این کامپوننت سرویس پیام‌ها را در اختیار قالب خود قرار داده و همچنین یک دکمه‌ی بستن را نیز به همراه دارد که خاصیت isDisplayed  آن‌را false می‌کند.


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

ابتدا به فایل src\app\product\product-edit\product-edit.component.ts مراجعه کرده و سرویس جدید پیام‌ها را به سازنده‌ی آن تزریق می‌کنیم:
import { MessageService } from './../../message/message.service';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.css']
})
export class ProductEditComponent implements OnInit {

  constructor(private productService: ProductService,
    private messageService: MessageService,
    private route: ActivatedRoute,
    private router: Router) { }
سپس ابتدای متد onSaveComplete آن‌را جهت درج پیام‌های این کامپوننت تغییر می‌دهیم.
  onSaveComplete(message?: string): void {
    if (message) {
      this.messageService.addMessage(message);
    }


تنظیم مسیرهای ثانویه

نحوه‌ی تعریف مسیریابی‌های مرتبط با router-outletهای غیراصلی برنامه، همانند سایر مسیریابی‌های برنامه‌است؛ با این تفاوت که در اینجا خاصیت outlet نیز به تنظیمات مسیر اضافه خواهد شد. به این ترتیب مشخص خواهیم کرد که محتوای این مسیر باید دقیقا در کدام router-outlet نامدار، درج شود.
برای این منظور فایل src\app\message\message-routing.module.ts را گشوده و تنظیمات مسیریابی آن‌را که به صورت RouterModule.forChild تعریف می‌شوند (چون ماژول اصلی برنامه نیستند)، تکمیل خواهیم کرد:
const routes: Routes = [
  { path: 'messages', component: MessageComponent, outlet: 'popup' }
];
همانطور که مشاهده می‌کنید، تنها تفاوت آن‌ها با سایر تعاریف مسیریابی‌های برنامه، ذکر نام Outlet ایی است که باید قالب MessageComponent را نمایش دهد.


فعالسازی یک مسیر ثانویه

در اینجا نیز همانند سایر مسیریابی‌ها، از دایرکتیو routerLink برای فعالسازی مسیرهای ثانویه استفاده می‌کنیم؛ اما syntax آن کمی متفاوت است:
<a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Messages</a>

<a [routerLink]="['/products', product.id, 'edit', { outlets: { popup: ['summary', product.id] } }]">Messages</a>
در اینجا می‌توان سبب فعال شدن چندین outlet به صورت همزمان شد. به همین جهت از نام جمع outlets استفاده شده‌است. سپس در ادامه key/valueهایی که بیانگر نام outlet و سپس path آن‌ها هستند، ذکر می‌شوند.
در دومین لینک تعریف شده، ابتدا یک مسیر اصلی فعال شده و سپس یک مسیر ثانویه نمایش داده می‌شود.

یک نکته: هرچند به primary outlet نامی انتساب داده نمی‌شود، اما نام آن دقیقا primary است و می‌توان قسمت outlets را به صورت ذیل نیز تعریف کرد:
{ outlets: { primary: ['/products', product.id,'edit'], popup: ['summary', product.id] }}


در ادامه فایل src\app\app.component.html را ویرایش کرده و لینک Show Messages را به آن اضافه می‌کنیم:
    <ul class="nav navbar-nav navbar-right">
      <li *ngIf="authService.isLoggedIn()">
        <a>Welcome {{ authService.currentUser.userName }}</a>
      </li>
      <li>
         <a [routerLink]="[{ outlets: { popup: ['messages'] } }]">Show Messages</a>
      </li>
که سبب نمایش لینک Show Messages در منوی بالای سایت می‌شود (تصویر فوق). در این حال اگر بر روی آن کلیک کنیم این پنل جدید به سمت راست صفحه اضافه می‌شود. برای آزمایش آن، محصولی را ویرایش کنید، تا پیام مرتبط با آن در این پنل نمایش داده شود.
آدرس آن نیز چنین شکلی را پیدا می‌کند:
 http://localhost:4200/products(popup:messages)
در اینجا مسیرثانویه داخل یک پرانتز نمایش داده شده‌است. در این حالت اگر به صفحات مختلف برنامه مراجعه کنیم، هنوز این قسمت داخل پرانتز حفظ می‌شود و نمایان خواهد بود.

اکنون می‌خواهیم قابلیت مخفی سازی این پنل را نیز پیاده سازی کنیم. به همین جهت از خاصیت isDisplayed سرویس پیام‌ها که توسط دکمه‌ی بستن MessageComponent مدیریت می‌شود، استفاده خواهیم کرد. بنابراین لینک جدیدی را که در فایل src\app\app.component.html اضافه کردیم، به نحو ذیل تغییر خواهیم داد:
      <li *ngIf="!messageService.isDisplayed">
          <a (click)="displayMessages()">Show Messages</a>
      </li>
      <li *ngIf="messageService.isDisplayed">
         <a (click)="hideMessages()">Hide Messages</a>
      </li>
ngIfها بر اساس مقدار isDisplayed، سبب درج و یا حذف لینک‌های نمایش و مخفی کردن پیام‌ها می‌شوند و چون این قالب اکنون از سرویس پیام‌ها استفاده می‌کند، نیاز است این سرویس را به کامپوننت آن نیز تزریق کنیم:

import { MessageService } from './message/message.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  constructor(private authService: AuthService,
    private router: Router,
    private messageService: MessageService) {
  }

  displayMessages(): void {
    this.router.navigate([{ outlets: { popup: ['messages'] } }]);
    this.messageService.isDisplayed = true;
  }

  hideMessages(): void {
    this.router.navigate([{ outlets: { popup: null } }]);
    this.messageService.isDisplayed = false;
  }
}
در اینجا تزریق سرویس پیام‌ها را به سازنده‌ی کامپوننت App مشاهده می‌کنید. همچنین دو متد جدید نمایش و مخفی سازی پیام‌ها نیز تعریف شده‌اند که این متدها در قالب این کامپوننت، به لینک‌های مرتبطی متصل هستند.
برای فعالسازی یک مسیرثانویه توسط متدهای برنامه، نیاز است از سرویس مسیریاب و متد navigate آن استفاده کرد که نمونه‌هایی از آن‌را در اینجا ملاحظه می‌کنید. پارامترهای ذکر شده‌ی در اینجا نیز همانند دایرکتیو routerLink هستند.

یک نکته: اگر به متد hideMessages دقت کنید، مقدار value کلید popup به نال تنظیم شده‌است. این مورد سبب خواهد شد تا outlet آن خالی شود. به این ترتیب متد hideMessages علاوه بر مخفی کردن لینک نمایش پیام‌ها، پنل آن‌را نیز از صفحه حذف می‌کند. شبیه به همین نکته در متد close کامپوننت پیام‌ها که دکمه‌ی بستن آن‌را به همراه دارد، پیاده سازی شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت هشتم- تعریف سطوح دسترسی پیچیده
با توجه به مفهوم  Role Claims  که در مطلب  Asp.net Core Identity عنوان شد، چگونه می‌توان این سطوح دسترسی پویا را با Identity Server  پیاده سازی کرد؟ ( منظور پیاده سازی سطوح دسترسی طبق پاراگراف «وقتی کاربری عضو یک نقش است، به صورت خودکار Role Claims آن نقش را نیز به ارث می‌برد. هدف از نقش‌ها، گروه بندی کاربران است. توسط Role Claims می‌توان مشخص کرد این نقش‌ها چه کارهایی را می‌توانند انجام دهند.» است.)
مطالب
آشنایی با Saltarelle کامپایلر قدرتمند #C به جاوااسکریپت

شاید ساده‌ترین تعریف برای  Saltarelle  این باشد که «کامپایلریست که کد‌های C# را به جاوا اسکریپت تبدیل می‌کند». محاسن زیادی را می‌توان برای اینگونه کامپایلر‌ها نام برد؛ مخصوصا در پروژه‌های سازمانی که نگهداری از کد‌های جاوا اسکریپت بسیار سخت و گاهی خارج از توان است و این شاید مهمترین عامل ظهور ابزارهای جدید از قبیل Typescript باشد.

در هر صورت اگر حوصله و وقت کافی برای تجهیز تیم نرم افزاری، به دانش یک زبان جدید مانند Typescript نباشد، استفاده از توان و دانش تیم تولید، از زبان C# ساده‌ترین راه حل است و اگر ابزاری مطمئن برای استفاده از حداکثر قدرت JavaScript همراه با امکانات نگهداری و توسعه کد‌ها وجود داشته باشد، بی شک Saltarelle یکی از بهترین‌های آنهاست.

قبلا کامپایلر هایی از این دست مانند  Script# وجود داشتند، اما فاقد همه امکانات C# بوده وعملا قدرت کامل C# در کد نویسی وجود نداشت. اما با توجه به ادعای توسعه دهندگان این کامپایلر سورس باز در استفاده‌ی حداکثری از کلیه ویژگی‌های C# 5 و با وجود Library ‌های متعدد می‌توان Saltarelle  را عملا یک کامپایلر موفق در این زمینه دانست.

برای استفاده از Saltarelle در یک برنامه وب ساده باید یک پروژه Console Application به Solution اضافه کرد و پکیج Saltarelle.Compiler را از nuget نصب نمایید. بعد از نصب این پکیج، کلیه Reference ‌ها از پروژه حدف می‌شوند و هر بار Build توسط کامپایلر Saltarelle  انجام می‌شود. البته با اولین Build، مقداری Error را خواهید دید که برای از بین بردنشان نیاز است پکیج Saltarelle.Runtime را نیز در این پروژه نصب نمایید:

PM> Install-Package Saltarelle.Compiler
PM> Install-Package Saltarelle.Runtime

در صورتیکه کماکان Build  نهایی با Error همرا بود، یکبار این پروژه را Unload  و سپس مجددا Load نمایید



UI یک پروژه وب MVC است و Client یک Console Application که پکیج‌های مورد نیاز Saltarelle  روی آن نصب شده است.

در صورتیکه پروژه را Build نماییم و نگاهی به پوشه‌ی Debug بیاندازیم، یک فایل JavaScript همنام پروژه وجود دارد:


برای اینکه بعد از هر بار Build ، فایل اسکریپت به پوشه‌ی مربوطه در پروژه UI منتقل شود کافیست کد زیر را در Post Build  پروژه Client بنویسیم: 

copy "$(TargetDir)$(TargetName).js" "$(SolutionDir)SalratelleSample.UI\Scripts"

اکنون پس از هر بار Build ، فایل اسکریپت مورد نظر در پوشه‌ی Scripts پروژه UI  آپدیت می‌شود:


در ادامه کافیست فایل اسکریپت را به layout اضافه کنیم. 

<script src="~/Scripts/SaltarelleSample.Client.js"></script>

در پوشه‌ی Saltarelle.Runtime در پکیج‌های نصب شده، یک فایل اسکریپت به نام mscorlib.min.js نیز وجود دارد که حاوی اسکریپت‌های مورد نیاز Saltarelle در هنگام اجراست. آن را به پوشه اسکریپت‌های پروژه UI کپی نمایید و سپس به Layout  اضافه کنید. 

<script src="~/Scripts/mscorlib.min.js"></script>
<script src="~/Scripts/SaltarelleSample.Client.js"></script>

حال نوبت به اضافه نمودن library‌های مورد نیازمان است. برای دسترسی به آبجکت هایی از قبیل document, window, element و غیره در جاوااسکریپت می‌توان پکیج Saltarelle.Web را در پروژه‌ی Client نصب نمود و برای دسترسی به اشیاء و فرمانهای jQuery، پکیج Salratelle.jQuery را نصب نمایید. 

> Install-Package Saltarelle.Web
> Install-Package Saltarelle.jQuery

به این library‌ها imported library می‌گویند. در واقع، در زمان کامپایل، برای این library‌ها فایل اسکریپتی تولید نمی‌شود و فقط آبجکت‌های #C هستند که که هنگام کامپایل تبدیل به کدهای ساده اسکریپت می‌شوند که اگر اسکریپت مربوط به آنها به صفحه اضافه نشده باشد، اجرای اسکریپت با خطا مواجه می‌شود.

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

<script src="~/Scripts/jquery-1.10.2.min.js"></script>

مثال ما یک اپلیکیشن ساده برای خواندن فید‌های همین سایت است. ابتدا کد‌های سمت سرور را در پروژه UI  می نویسیم.

کلاس‌های مورد نیاز ما برای این فید ریدر: 

public class Feed
    {
        public string FeedId { get; set; }
        public string Title { get; set; }
        public string Address { get; set; }

    }
    public class Item
    {
        public string Title { get; set; }
        public string Link { get; set; }
        public string Description { get; set; }
    }

و یک کلاس برای مدیریت منطق برنامه 

 public class SiteManager
    {
        private static List<Feed> _feeds;
        public static List<Feed> Feeds
        {
            get
            {
                if (_feeds == null)
                    _feeds = CreateSites();
                return _feeds;
            }
        }
        private static List<Feed> CreateSites()
        {
            return new List<Feed>() { 
                new Feed(){
                    FeedId = "1",
                    Title = "آخرین تغییرات سایت",
                    Address = "https://www.dntips.ir/rss.xml"
                },
                 new Feed(){
                    FeedId = "2",
                    Title = "مطالب سایت",
                    Address = "https://www.dntips.ir/feeds/posts"
                },
                 new Feed(){
                    FeedId = "3",
                    Title = "نظرات سایت",
                    Address = "https://www.dntips.ir/feeds/comments"
                },
                 new Feed(){
                    FeedId = "4",
                    Title = "خلاصه اشتراک ها",
                    Address = "https://www.dntips.ir/feed/news"
                },
            };
        }

        public static IEnumerable<Item> GetNews(string id)
        {
            XDocument feedXML = XDocument.Load(Feeds.Find(s=> s.FeedId == id).Address);
            var feeds = from feed in feedXML.Descendants("item")
                        select new Item
                        {
                            Title = feed.Element("title").Value,
                            Link = feed.Element("link").Value,
                            Description = feed.Element("description").Value
                        };
            return feeds;
        }

    }

کلاس SiteManager فقط یک لیست از فید‌ها دارد و متدی که با گرفتن شناسه‌ی فید ، یک لیست از آیتم‌های موجود در آن فید ایجاد می‌کند.

حال دو ApiController برای دریافت داده‌ها ایجاد می‌کنیم

public class FeedController : ApiController  
{
        // GET api/<controller>
        public IEnumerable<Feed> Get()
        {
            return SiteManager.Feeds;
        }
    }

public class ItemsController : ApiController
    {
        // GET api/<controller>/5
        public IEnumerable<Item> Get(string id)
        {
            return SiteManager.GetNews(id);
        }
    }

در View پیش‌فرض که Index از کنترلر Home  است،  یک Html ساده برای فرم  صفحه اضافه می‌کنیم 

<div>
    <div>
        <h2>Feeds</h2>
        <ul id="Feeds">
           
        </ul>
    </div>
    <div>
        <h2>Items</h2>
        <p id="FeedItems">
        </p>
    </div>
   
</div>

در المنت Feeds لیست فید‌ها را قرار می‌دهیم و در FeedItems آیتم‌های مربوط به هر فید. حال به سراغ کد‌های سمت کلاینت می‌رویم و به جای جاوا اسکریپت از Saltarelle استفاده می‌کنیم.

کلاس Program را از پروژه Client باز می‌کنیم و متد Main را به شکل زیر تغییر می‌دهیم:

static void Main()
        {
            jQuery.OnDocumentReady(() => {
                FillFeeds();
            });
        }

بعد از کامپایل شدن، کد #C شارپ بالا به صورت زیر در می‌آید: 

$SaltarelleSample_Client_$Program.$main = function() {
$(function() {
$SaltarelleSample_Client_$Program.$fillFeeds();
});
};
$SaltarelleSample_Client_$Program.$main();

و این همان متد معروف jQuery است که Saltarelle.jQuery برایمان ایجاد کرده است.

متد FillFeeds را به شکل زیر پیاده سازی می‌کنیم

private static void FillFeeds()
        {
            jQuery.Ajax(new jQueryAjaxOptions()
            {
                Url = "/api/feed",
                Type = "GET",
                Success = (d,t,r) => {

                    // Fill 
                    var ul = jQuery.Select("#Feeds");
                    jQuery.Each((List<Feed>)d, (idx,i) => {
                        var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer");
                        li.Click(eve => {
                            FillData(i.FeedId);
                        });
                        ul.Append(li);
                    });
                }
            });
        }

آبجکت jQuery، متدی به نام Ajax دارد که یک شی از کلاس jQueryAjaxOptions را به عنوان پارامتر می‌پذیرد. این کلاس کلیه خصوصیات متد Ajax در jQuery را پیاده سازی می‌کند. نکته شیرین آن توانایی نوشتن lambda برای Delegate هاست.

خاصیت Success یک Delegate است که 3 پارامتر ورودی را می‌پذیرد.

public delegate void AjaxRequestCallback(object data, string textStatus, jQueryXmlHttpRequest request);

data همان مقداریست که api باز می‌گرداند که یک لیست از Feed هاست. برای زیبایی کار، من یک کلاس Feed در پروژه Client اضافه می‌کنم که خصوصیاتی مشترک با کلاس اصلی سمت سرور دارد و مقدار برگشی Ajax را به آن تبدیل می‌کنم.

کلاس Feed و Item

 [PreserveMemberCase()]
    public class Feed
    {
        //[ScriptName("FeedId")]
        public string FeedId;

        //[ScriptName("Title")]
        public string Title;

        //[ScriptName("Address")]
        public string Address;

    }

    [PreserveMemberCase()]
    public class Item
    {
        // [ScriptName("Title")]
        public string Title;

        // [ScriptName("Link")]
        public string Link;

        // [ScriptName("Description")]
        public string Description;
    }
Attrubute‌های زیادی در Saltarelle وجود دارند و از آنجایی که کامپایلر اسم فیلد‌ها را camelCase کامپایل می‌کند من برای جلوگیری از آن از PreserveMemberCase  بر روی هر کلاس استفاده کردم. می‌توانید اسم هر فیلد را سفارشی کامپایل نمایید. 
jQuery.Each((List<Feed>)d, (idx,i) => {
                        var li = jQuery.Select("<li>").Text(i.Title).CSS("cursor", "pointer");
                        li.Click(eve => {
                            FillData(i.FeedId);
                        });
                        ul.Append(li);
                    });

به ازای هر آیتمی که در شیء بازگشتی وجود دارد، با استفاد از متد each در jQuery یک li ایجاد می‌کنیم. همان طور که می‌بینید کلیه خواص، به شکل Fluent قابل اضافه شدن می‌باشد. سپس برای li یک رویداد کلیک که در صورت وقوع، متد FillData را با شناسه فید کلیک شده فراخوانی می‌کند و در آخر li را به المنت ul اضافه می‌کنیم.

برای هر کلیک هم مانند مثال بالا api را با شناسه‌ی فید مربوطه فراخوانی کرده و به ازای هر آیتم، یک سطر ایجاد می‌کنیم.

private static void FillData(string p)
        {
            jQuery.Ajax(new jQueryAjaxOptions()
            {
                Url = "/api/items/" + p,
                Type = "GET",
                Success = (d, t, r) => {
                    var content = jQuery.Select("#FeedItems");
                    content.Html("");
                    foreach (var item in (List<Item>)d)
                    {
                        var row = jQuery.Select("<div>").AddClass("row").CSS("direction", "rtl");
                        var link = jQuery.Select("<a>").Attribute("href", item.Link).Text(item.Title);
                        row.Append(link);
                        content.Append(row);
                    }
                }
            });
        }
خروجی برنامه به شکل زیر است: 

در این مثال ما از Saltarelle.jQuery برای استفاده از jQuery.js استفاده نمودیم. library‌های متعددی برای Saltarelle  از قبیل  linq,angular,knockout,jQueryUI,nodeJs ایجاد شده و همچنین قابلیت‌های زیادی برای نوشتن imported library‌های سفارشی نیز وجود دارد. 

مطمئنا استفاده از چنین کامپایلرهایی راه حلی سریع برای رهایی از مشکلات متعدد کد نویسی با جاوا اسکریپت در نرم افزارهای بزرگ مقیاس است. اما مقایسه آنها با ابزارهایی از قبیل typescript احتیاج به زمان و تجربه کافی در این زمینه دارد.

مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت سوم - Data binding
در قسمت قبل، ساختار فرم ثبت اطلاعات کارمندان را تکمیل کردیم. در این قسمت قصد داریم این اطلاعات را در کامپوننت آن توسط data binding دریافت کنیم.


نقش ngModel در data binding

ngModel دایرکتیوی است که وجود آن سبب می‌شود تا Angular آن المان ورودی خاص را تحت نظر قرار دهد:
<!--no binding -->
<input name="firstname" ngModel>
در حالت تعریفی فوق، هیچگونه عملیات data binding ایی صورت نمی‌گیرد؛ اما Angular به علت وجود ngModel، از وجود این فیلد مطلع شده‌است. اما کامپوننت برنامه اطلاعات خاصی را دریافت نخواهد کرد.
برای رفع این مشکل می‌توان با data binding یک طرفه شروع کرد:
<!--one way binding -->
<input name="firstname" [ngModel]="firstName">
در اینجا از syntax ویژه‌ی property binding استفاده شده و ngModel داخل [] قرار گرفته‌است و به firstName تنظیم شده‌است. در این حالت Angular در کامپوننت متناظر با این قالب HTML ایی، به دنبال یک خاصیت عمومی به نام firstName می‌گردد و مقدار اولیه‌ی این فیلد را از آن دریافت می‌کند.
در حالت data binding یک طرفه، اگر کاربر اطلاعات فیلد firstname را در فرم برنامه تغییر دهد، این اطلاعات به خاصیت عمومی firstName منعکس نخواهد شد.
برای رفع این مشکل (در صورت نیاز)، می‌توان از data binding دو طرفه استفاده کرد:
<!--two way binding -->
<input name="firstname" [ngModel]="firstName"
(ngModelChange)="firstName=$event">
این حالت شبیه به حالت data binding یک طرفه است؛ با این تفاوت که رویدادگردانی ngModelChange نیز به آن اضافه شده‌است. در اینجا event$ به مقدار فیلد تغییر یافته اشاره می‌کند و آن‌را به firstName انتساب می‌دهد.
البته این حالت دو طرفه، syntax ساده شده‌ی زیر را که به banana in the box نیز معروف شده‌است (موز همان () است و جعبه به [] اشاره می‌کند)، نیز می‌تواند داشته باشد که بیشتر مورد استفاده قرار می‌گیرد:
<!--two way binding -->
<input name="firstname" [(ngModel)]="firstName">


تعریف مدل فرم ثبت اطلاعات کارمندان

برای نگهداری اطلاعات فرم کارمندان، کلاس Employee را به ماژول Employee اضافه می‌کنیم:
 > ng g cl Employee/Employee
با این خروجی:
 installing class
  create src\app\Employee\employee.ts
سپس ساختار این کلاس را به نحو ذیل تکمیل خواهیم کرد که هر کدام از خواص آن، معادل یکی از المان‌های فرم است:
export class Employee {
  constructor(
    public firstName: string,
    public lastName: string,
    public isFullTime: boolean,
    public paymentType: string,
    public primaryLanguage: string
  ) {}
}
TypeScript این امکان را می‌دهد تا بتوان خواص عمومی را مستقیما در سازنده‌ی کلاس تعریف کرد. بنابراین در اینجا برای نمونه firstName هم یکی از آرگومان‌های سازنده‌ی کلاس کارمند است و هم یک خاصیت عمومی تعریف شده‌ی در آن. به علاوه در اینجا می‌توان به این خواص، مقادیر پیش فرضی را نیز انتساب داد تا در حین وهله سازی آن بتوان از تعریف اجباری یک سری از پارامترها صرفنظر کرد.

پس از آن، به فایل employee-register.component.ts مراجعه کرده و وهله‌ای از کلاس را به صورت یک خاصیت عمومی در اختیار قالب HTML ایی آن که فرم جاری را تشکیل می‌دهد، قرار می‌دهیم:
import { Employee } from "app/employee/employee";

export class EmployeeRegisterComponent implements OnInit {
  languages = ["Persian", "English", "Spanish", "Other"];
  model = new Employee("Vahid", "N", true, "FullTime", "Persian");
ابتدا کلاس کارمند import شده و سپس وهله‌ای از آن به نام model، به صورت یک خاصیت عمومی در اختیار قالب آن قرار گرفته‌است.


تغییر قالب فرم ثبت اطلاعات کارمندان برای اتصال به model

در ادامه، مرحله به مرحله قالب فرم جاری را جهت اتصال به شیء model فوق تغییر خواهیم داد:

اتصال به Text boxes

  <form #form="ngForm" novalidate>
    <div class="form-group">
      <label>First Name</label>
      <input type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
    </div>

    <div class="form-group">
      <label>Last Name</label>
      <input type="text" class="form-control" name="lastName" [(ngModel)]="model.lastName">
    </div>
همانطور که مشاهده می‌کنید، اینبار ngModel خالی قسمت قبل را توسط syntax تکمیلی banana in the box به data binding دو طرفه تغییر داده‌ایم. به این ترتیب در ابتدای نمایش فرم، این دو فیلد، مقادیر اولیه نام و نام خانوادگی را از شیء model دریافت کرده و نمایش می‌دهند. به علاوه اگر فرم نیز تغییر کند، این اطلاعات به شیء model و خواص آن نیز منعکس می‌شوند.

برای بررسی این مورد، در پایان فرم جهت دیباگ data binding، اطلاعاتی را که در مدل داریم و همچنین اطلاعاتی را که Angular در حال نظارت بر آن‌ها است، به صورت json در صفحه درج می‌کنیم:
    <button class="btn btn-primary" type="submit">Ok</button>
  </form>
  Model: {{ model | json }}
  <br> Angular: {{ form.value | json }}
  <br> form.pristine: {{ form.pristine }}
برای مثال یکبار [()] را به [] تبدیل کنید و سپس سعی در تغییر مقادیر فرم نمائید. مشاهده می‌کنید هرچند این اطلاعات تحت نظارت Angular هستند، اما چون data binding به حالت یک طرفه تغییر کرده‌است، دیگر انعکاس آن‌ها، در Model مشاهده نمی‌شوند.


اتصال به Check boxes

    <div class="checkbox">
      <label>
            <input type="checkbox" name="is-full-time"
                   [(ngModel)]="model.isFullTime"> Full Time Employee
            </label>
    </div>
روش کار در اینجا نیز همانند قبل است. با استفاده از data binding دو طرفه، مقدار checkbox را به یک خاصیت عمومی boolean انتساب داده‌ایم و برعکس (زمانیکه فرم برای بار اول نمایش داده می‌شود، مقدار اولیه‌ی خود را از شیء model دریافت می‌کند).

اتصال به Radio buttons

    <label>Payment Type</label>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="FullTime" checked
                   [(ngModel)]="model.paymentType">
                Full Time
            </label>
    </div>
    <div class="radio">
      <label>
            <input type="radio" name="paymentType" value="PartTime"
                   [(ngModel)]="model.paymentType">
                Part Time
            </label>
    </div>
روش اتصال به radio buttons نیز بر اساس data binding دو طرفه‌است. فقط در اینجا دقیقا یک خاصیت مشخص، به چندین radio button متصل شده‌است و در نهایت در این گروه که بر اساس name هایی یکسان تشکیل شده‌است، یک مقدار انتخاب می‌شود و مقدار آن از ویژگی value المان متناظر دریافت می‌گردد.

اتصال به Drop downs

    <div class="form-group">
      <label>Primary Language</label>
      <select class="form-control" name="primaryLanguage" [(ngModel)]="model.primaryLanguage">
          <option *ngFor="let lang of languages">
             {{ lang }}
          </option>
      </select>
    </div>
در اینجا نیز ابتدا نامی به این المان انتساب داده شده‌است و سپس توسط data binding دو طرفه، خاصیت متناظری از مدل را به این المان متصل کرده‌ایم یا برعکس؛ زمانیکه این فرم برای اولین بار نمایش داده می‌شود، مقدار اولیه‌ی این فیلد بر اساس مقدار آن در شیء model تعیین می‌شود:



نحوه‌ی فراخوانی یک متد در حین data binding دو طرفه

همانطور که در ابتدای بحث نیز عنوان شد، data binding دو طرفه را به نحو دیگری نیز می‌توان تعریف کرد:

<div class="form-group">
            <label>First Name</label>
            <input type="text" class="form-control" name="firstName" 
                [ngModel]="model.firstName" 
                (ngModelChange)="firstNameToUpperCase($event)">
</div>
در اینجا بجای استفاده‌ی از syntax معروف banana in the box، از روش اتصال یک طرفه و سپس دریافت تغییرات از طریق یک رخدادگردان استفاده شده‌است. مزیت این روش امکان دسترسی همزمان به مقدار وارد شده‌ی توسط کاربر، در کامپوننت متناظر می‌باشد:
  firstNameToUpperCase(value: string) {
    if (value.length > 0)
      this.model.firstName = value.charAt(0).toUpperCase() + value.slice(1);
    else
      this.model.firstName = value;
  }
برای مثال در اینجا اگر کاربر حرف اول یک نام را با حروف کوچک وارد کند، توسط این متد به حرف بزرگ تبدیل شده و جایگزین می‌شود. این جایگزینی نیز بلافاصله در فرم منعکس خواهد شد.

در قسمت بعد مباحث اعتبارسنجی فرم‌های مبتنی بر قالب‌ها را بررسی می‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-03.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.