نظرات مطالب
آشنایی با NHibernate - قسمت هشتم
با تشکر از تذکر شما.ولی کد زیر هم در صورت بروز خطا چیزی را برنمی گرداند. یعنی متد Delete چه با موفقیت به پایان برسد یا خطایی در حذف رکورد رخ دهد باز ;return true اجرا می شود.!؟
using(var repository = new Repository())
{
repository.Delete(myClass);
return true;
}
مطالب دوره‌ها
تغییر ترتیب آیتم‌های یک لیست به کمک افزونه jquery.sortable در ASP.NET MVC
در این مطلب قصد داریم ترتیب عناصر نمایش داده شده توسط یک لیست را به کمک افزونه بسیار سبک وزن jquery.sortable تغییر داده و نتایج را در سمت سرور مدیریت کنیم. این افزونه بر اساس امکانات کشیدن و رها ساختن HTML5 کار می‌کند و با مرورگرهای IE8 به بعد سازگار است.

مدل‌های برنامه

using System.Collections.Generic;

namespace jQueryMvcSample05.Models
{
    public class Survey
    {
        public int Id { set; get; }
        public string Title { set; get; }

        public virtual ICollection<SurveyItem> SurveyItems { set; get; }
    }
}

namespace jQueryMvcSample05.Models
{
    public class SurveyItem
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public int Order { set; get; }

        //[ForeignKey("SurveyId")]
        public virtual Survey Survey { set; get; }
        public int SurveyId { set; get; }
    }
}
به کمک این ساختار قصد داریم اطلاعات یک سیستم نظر سنجی را نمایش دهیم.
تعدادی نظر سنجی به همراه گزینه‌های آن‌ها تعریف خواهند شد (یک رابطه one-to-many است). سپس توسط افزونه sortable می‌خواهیم ترتیب قرارگیری گزینه‌های آن‌را مشخص کنیم یا تغییر دهیم.


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


using System.Collections.Generic;
using jQueryMvcSample05.Models;

namespace jQueryMvcSample05.DataSource
{
    /// <summary>
    /// یک منبع داده فرضی جهت دموی ساده‌تر برنامه
    /// </summary>
    public static class SurveysDataSource
    {
        private static IList<Survey> _surveysCache;
        static SurveysDataSource()
        {
            _surveysCache = createSurveys();
        }

        public static IList<Survey> SystemSurveys
        {
            get { return _surveysCache; }
        }

        private static IList<Survey> createSurveys()
        {
            var results = new List<Survey>();
            for (int i = 1; i < 6; i++)
            {
                results.Add(new Survey
                {
                    Id = i,
                    Title = "نظر سنجی " + i,
                    SurveyItems = new List<SurveyItem>
                    {
                       new SurveyItem{ Id = 1, SurveyId = i, Title = "گزینه 1", Order = 1 },
                       new SurveyItem{ Id = 2, SurveyId = i, Title = "گزینه 2", Order = 2 },
                       new SurveyItem{ Id = 3, SurveyId = i, Title = "گزینه 3", Order = 3 },
                       new SurveyItem{ Id = 4, SurveyId = i, Title = "گزینه 4", Order = 4 }
                    }
                });
            }
            return results;
        }
    }
}
در اینجا نیز از یک منبع داده فرضی تشکیل شده در حافظه جهت سهولت دموی برنامه استفاده خواهد شد.


کدهای کنترلر برنامه

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample05.DataSource;
using jQueryMvcSample05.Security;

namespace jQueryMvcSample05.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var surveysList = SurveysDataSource.SystemSurveys;
            return View(surveysList);
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult SortItems(int? surveyId, string[] items)
        {
            if (items == null || items.Length == 0 || surveyId == null)
                return Content("nok");

            updateSurvey(surveyId, items);

            return Content("ok");
        }

        /// <summary>
        /// این متد جهت آشنایی با پروسه به روز رسانی ترتیب گزینه‌ها در اینجا قرار گرفته است
        /// بدیهی است محل قرارگیری آن باید در لایه سرویس برنامه اصلی باشد
        /// </summary>
        private static void updateSurvey(int? surveyId, string[] items)
        {
            var itemIds = new List<int>();
            foreach (var item in items)
            {
                itemIds.Add(int.Parse(item.Replace("item-row-", string.Empty)));
            }

            var survey = SurveysDataSource.SystemSurveys.FirstOrDefault(x => x.Id == surveyId.Value);
            if (survey == null)
                return;

            int order = 0;
            foreach (var itemId in itemIds)
            {
                order++;
                var surveyItem = survey.SurveyItems.FirstOrDefault(x => x.Id == itemId);
                if (surveyItem == null) continue;
                surveyItem.Order = order;
            }

            //todo: call save changes ....
        }
    }
}

و کدهای View متناظر

@model IList<jQueryMvcSample05.Models.Survey>
@{
    ViewBag.Title = "Index";
    var sortUrl = Url.Action(actionName: "SortItems", controllerName: "Home");
}
<h2>
    نظر سنجی‌ها</h2>
@foreach (var survey in Model)
{
    <fieldset>
        <legend>@survey.Title</legend>
        <div id="sortable-@survey.Id">
            @foreach (var surveyItem in survey.SurveyItems.OrderBy(x => x.Order))
            {            
                <div id="item-row-@surveyItem.Id">
                    <span class="handles">::</span>
                    @surveyItem.Title
                </div>
            }
        </div>
    </fieldset>
}
<div>
    لطفا برای تغییر ترتیب آیتم‌های تعریف شده، از امکان کشیدن و رها کردن تعریف شده بر
    روی آیکون‌های :: در کنار هر آیتم استفاده نمائید.
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $('div[id^="sortable"]').sortable({ handle: 'span' }).bind('sortupdate', function (e, ui) {
                var sortableItemId = $(ui.item).parent().attr('id');
                var surveyId = sortableItemId.replace('sortable-', '');
                var items = [];
                $('#' + sortableItemId + ' div').each(function () {
                    items.push($(this).attr('id'));
                });
                //alert(items.join('&'));                
                $.ajax({
                    type: "POST",
                    url: "@sortUrl",
                    data: JSON.stringify({ items: items, surveyId: surveyId }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = "/login";
                        } else if (status === 'error' || !data || data == "nok") {
                            alert('خطایی رخ داده است');
                        }
                        else {
                            alert('انجام شد');
                        }
                    }
                });
            });
        });
    </script>
}
توضیحات

در اینجا نیاز بود تا ابتدا کدهای کنترلر و View ارائه شوند، تا بتوان در مورد ارتباطات بین آن‌ها بهتر بحث کرد.
در ابتدای نمایش صفحه Home، رکوردهای نظرسنجی‌ها از منبع داده دریافت شده و به View ارسال می‌شوند. در View برنامه یک حلقه تشکیل گردیده و این موارد رندر خواهند شد.
هر نظر سنجی با یک div بیرونی که با id مساوی sortable شروع می‌شود، آغاز گردیده و گزینه‌های آن نظر سنجی نیز توسط divهایی با id مساوی item-row شروع خواهند گردید. هر کدام از این idها حاوی id رکوردهای متناظر هستند. از این id‌ها در کدهای برنامه جهت یافتن یک نظر سنجی یا یک ردیف مشخص برای به روز رسانی ترتیب آن‌ها استفاده خواهیم کرد.
ادامه کار، به تنظیمات و اعمال افزونه sortable مرتبط می‌شود. توسط تنظیم ذیل به jQuery اعلام خواهیم کرد، هرجایی یک div با id شروع شده با sortable یافتی، افزونه sortable را به آن متصل کن:
 $('div[id^="sortable"]').sortable
در ادامه در ناحیه و  div ایی که عمل کشیدن و رها شدن رخ داده، id این div را بدست آورده و سپس کلیه row-itemهای آن را در آرایه‌ای به نام items قرار می‌دهیم:
 var sortableItemId = $(ui.item).parent().attr('id');
var surveyId = sortableItemId.replace('sortable-', '');
var items = [];
$('#' + sortableItemId + ' div').each(function () {
  items.push($(this).attr('id'));
});
اکنون که به id یک نظر سنجی و همچنین idهای ردیف‌های مرتب شده حاصل دسترسی داریم، آن‌ها را توسط jQuery Ajax به کنترلر برنامه ارسال می‌کنیم:
 data: JSON.stringify({ items: items, surveyId: surveyId })
امضای اکشن متد SortItems نیز دقیقا بر همین مبنا تنظیم شده است:
 public ActionResult SortItems(int? surveyId, string[] items)

اطلاعاتی که در اینجا دریافت می‌شوند در متد updateSurvey مورد استفاده قرار خواهند گرفت. بر اساس surveyId دریافتی، نظرسنجی مرتبط را یافته و سپس به گزینه‌های آن دست خواهیم یافت. اکنون نوبت به پردازش آرایه items دریافت شده است. این آرایه بر اساس انتخاب کاربر مرتب شده است.


دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample05.zip
 
مطالب
برنامه نویسی اندروید با Xamarin.Android - قسمت سوم
در این مقاله می‌خواهیم یک لیست ساده را ایجاد کرده و داخل یک کنترل (View)، از نوع ListView قرار دهیم. همچنین با برخی از کنترل‌های پرکاربرد، برای چیدمان کنترل‌ها در اندروید آشنا می‌شویم.

قبل از شروع به طراحی UI باید کمی با واحدهای اندازه گیری در اندروید آشنا شویم. بدانید و آگاه باشید که استفاده از واحد Pixel برای تعیین اندازه در اندروید کار بسیار اشتباهی است. طراح همیشه باید Density یا تراکم صفحه‌ی نمایش را در نظر بگیرد. تراکم صفحه‌ی نمایش به معنای تعداد پیکسل موجود در یک اینچ می‌باشد. اندازه‌ی 100 پیکسل در دستگاه‌های مختلف با (dpi(Dot Per Inchهای متفاوت به یک اندازه نیست.

واحد dpi: اندروید واحد dpi را برای طراحی و چیدمان Layoutها معرفی کرده است. dpi مخفف Device Independent Pixel هست و معمولا بصورت dp نوشته می‌شود که یک واحد پیکسلی مجازی است و بر پایه‌ی یک صفحه نمایش با رزولوشن 160dpi طراحی شده‌است. به عبارت دیگر یک dp، یک پیکسل در یک صفحه‌ی نمایش با رزولوشن 160dpi می‌باشد. این واحد این اطمینان را به شما می‌دهد که یک View، در صفحه نمایش‌های با رزولوشن متفاوت، بطور مناسبی بزرگ یا کوچک می‌شود.

واحد sp: مخفف Scale Independent Pixel است و شبیه dp عمل می‌کند؛ با این تفاوت که تنظیمات کاربر را (مثلا شخصی که بخاطر ضعف چشم اندازه‌ی قلم گوشی خود را بزرگ نموده) در محاسبات خود در نظر می‌گیرد. به دلیل آنکه از لحاظ زیبایی شناسی و همچنین چیدمان عناصر داخل UI زمانیکه از واحد اندازه گیری sp استفاده می‌کنیم ممکن است با مشکل مواجه شویم، بیشتر از dp استفاده می‌کنیم، مگر در بعضی مواقع آن هم برای مقداردهی به اندازه‌ی قلم!

خوب! به سراغ فولدر Layout رفته و Main.axml را باز نمایید. به قسمت Source بروید.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/Hello" />
</LinearLayout>
در این سند axml یک LinearLayout مشاهده می‌نمایید. وقتی شما View را به LinearLayout اضافه می‌کنید، با توجه به اینکه orientation آن را vertical یا horizontal انتخاب کرده باشید، به صورت افقی و یا عمودی طرح بندی را انجام می‌دهد.

layout_width و layout_height (مقداردهی آن‌ها الزامی است) ابعاد layout ما را مشخص می‌کنند. مقدار fill_parent دیگر منسوخ شده و به جای آن match_parent استفاده می‌شود و به معنای آن است که تمام فضای موجود در کنترل را اشغال کند. مقدار دیگری که می‌توان به آن نسبت داد (و در layout_height مربوط به Button مشاهده می‌نمایید)، wrap_content می‌باشد که اعلام می‌کند فقط به میزان مورد نیاز برای محتویات، کنترل والد را اشغال کند. البته با تغییر میزان محتویات، اندازه‌ی کنترل متغییر است. شما می‌توانید مقادیر عددی را هم با واحد dp یا حتی pixel (که اصلا توصیه نمی‌شد) جایگزین نمایید.

در ادامه، کنترل (که در اندروید به آن View گفته می‌شود) Button را حذف نمایید و به جای آن یک ListView را قرار دهید و نامی را به آن نسبت دهید. ListView از کاربردی‌ترین و مهم‌ترین کنترل‌های اندروید می‌باشد. ListView شامل قسمت‌های زیر است:
Rows: قسمت نمایش دهنده‌ی داده‌ها.
Adapter: یک کلاس که وظیفه‌ی انقیاد منبع داده را به ListView، بر عهده دارد.
Fast Scrolling: یک دسته(handle) که به کاربر اجازه می‌دهد تا در طول ListView حرکت کند.
Section Index: یک view می‌باشد و جایگاه لیت را هنگام اسکرول مشخص میکند و معمولا در Contacts گوشی بصورت ابتدای حروف نام مخاطبین خود مشاهده کرده‌اید.
Layout زیر را در نظر بگیرید:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:background="#fff"
        android:id="@+id/NameListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>  
به MainActivity.cs بروید و کدهای مربوط به Button قبلی را که با ListView جایگزین کرده‌ایم، حذف نمایید. متد OnCreate به این صورت می‌باشد:
protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            List<string> namesList = new List<string>
            {
                "Mohammad","Fatemeh","Ali","Hasan","Husein","Mohsen","Mahdi",
            };
            var namesAdapter = new ArrayAdapter<string>
                (this, Android.Resource.Layout.SimpleListItem1, namesList);

            var listview = FindViewById<ListView>(Resource.Id.NameListView);
            listview.Adapter = namesAdapter;
        }
همانطور که گفته شد SetContentView مشخص کننده‌ی layout مورد نظر ما برای نمایش می‌باشد. می‌توان بدون هیچ layout خاصی با کدهای سی شارپ، کنترل‌های مورد نظر را ایجاد کرد که کار زمانبری است؛ ولی بعضی مواقع مجبور به این کار هستیم.
namesList یک لیست ساده از نوع string با مقدار دهی اولیه است.
ArrayAdapter یک کلاس Adapter توکار می‌باشد که یک آرایه (یا لیست) را از نوع string، برای نمایش به ListView متصل می‌کند (bind). نوع جنریک آن یعنی <ArrayAdapter<T برای نوع‌های دیگر هم استفاده می‌شود. در واقع Adapter با دریافت یک لیست برای نمایش و یک Layout برای تعیین نوع نمایش، به ازای هر سطر از اطلاعات یک View را با اطلاعات آن سطر به سمت ListView ارسال می‌کند. در اینجا ما در سازنده‌ی ArrayAdapter با استفاده از Resourceهای توکار اندروید که از طریق Android.Resource به آن‌ها دسترسی داریم، یک layout ساده را شامل یک TextView(مانند label و یا textBlock)، به همراه namesList، برای Adapter ارسال کردیم.
متد FindViewById با توجه به Layout معرفی شده‌ی به Activity، به دنبال View با Id مورد نظر می‌پردازد. مهم نیست که در Layoutهای جداگانه نام‌های یکسانی استفاده کنید. این متد در کلاس View قرار دارد و تمام کنترل(View)ها، فرزند آن می‌باشند. در اینجا از نوع جنریک آن استفاده شده که عمل تبدیل View به ListView را خود متد بر عهده بگیرد.
در انتها Adapter مورد نظر به ویژگی Adpater کنترل ListView اضافه می‌شود.

ListView کنترل بسیار منعطفی می‌باشد. برخی ویژگی‌ها آن را در زیر می‌توانید مشاهده بفرمایید:
  • android:dividerHeight                    // ارتفاع جداکننده‌ی سطرها
  • android:divider                            // رنگ جداکننده‌ی سطرها
  • android:layoutAnimation               // انیمیشن برای layoutها 
  • android:background                    // رنگ ضمینه را مشخص میکند. البته میتوانید یک style را به ان نسبت دهید

خوب؛ حالا بیایید یک ListView را با ظاهر و Adapter سفارشی بسازیم.
ابتدا باید یک Layout را طراحی کنیم تا به ازای هر سطر برای ListView ارسال شود. با استفاده از Add->New item یک Layout را به فولدر layout اضافه کنید.
کد زیر را درون فایل axml مربوطه کپی کنید. 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="14dp">
    <TextView
        android:text=""
        android:gravity="center_vertical"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/idTextView" />
    <TextView
        android:text=""
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/nameTextView"
        android:layout_marginLeft="14dp" />
</LinearLayout>
کلاس زیر (یا هر کلاس دلخواه دیگری) را به عنوان مدل برنامه اضافه کنید.
namespace DotSystem.ir.App1.Model
{
    public class Person
    {
        public int Id { get; set; }
        public string PersonName { get; set; }

    }
حالا باید Adapter خود را بسازیم. ابتدا کلاسی را با نام PersonAdapter به برنامه اضافه نمایید. این کلاس باید از کلاس BaseAdapter (نوع جنریک آن هم موجود می‌باشد) و یا فرزندان آن ArrayAdapter، CursorAdapter و ... ارث بری نماید. اگر مستقیما از BaseAdapter استفاده کنیم، به دلیل Abstract بودن تعدادی از متدها و Propertyها مجبور به override کردن آن‌ها می‌شویم. ما در اینجا از BaseAdapter استفاده می‌کنیم. کد زیر را در نظر بگیرید:
namespace DotSystem.ir.App1.Adapters
{
    public class PersonAdapter : BaseAdapter<Model.Person>
    {
        public override Person this[int position]
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public override int Count
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public override long GetItemId(int position)
        {
            throw new NotImplementedException();
        }

        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            throw new NotImplementedException();
        }
    }
}
BaseAdapter شامل یک Indexer برای دسترسی آسان به Itemهای لیست، یک ویژگی برای برگرداندن تعداد آیتم‌ها، متدی برای برگرداندن Id هر آیتم و مهمترین بخش آن یعنی متد GetView که برای نمایش هر آیتمی یک بار اجرا می‌شود و Layout مورد نظر ما را با اطلاعات پر کرده و به سمت ListView می‌فرستد.

در اینجا ما به چند فیلد داخل کلاس احتیاج داریم.
  • لیست اطلاعات مورد نظر.
  • Activity جاری که Adapter را استفاده می‌کند.
بنابراین دو فیلد را به همراه متد سازنده، برای مقدار دهی آن‌ها اضافه کرده و کلاس بالا را نیز تکمیل می‌کنیم.
namespace DotSystem.ir.App1.Adapters
{
    public class PersonAdapter : BaseAdapter<Person>
    {
        protected Activity _activity = null;
        protected List<Person> _list = null;
        public PersonAdapter(Activity activity, List<Person> list)
        {
            _activity = activity;
            _list = list;
        }
        public override Person this[int position]
        {
            get
            {
                return _list[position];
            }
        }

        public override int Count
        {
            get
            {
                return _list.Count;
            }
        }

        public override long GetItemId(int position)
        {
            return _list[position].Id;
        }

        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            throw new NotImplementedException();
        }
    }
}
در این مرحله باید متد GetView را پیاده سازی کنیم. به پیاده سازی زیر دقت کنید:
public override View GetView(int position, View convertView, ViewGroup parent)
        {
            if (convertView == null)
                convertView = _activity.LayoutInflater
                    .Inflate(Resource.Layout.PersonListViewItemLayout, parent, false);

            var idTextView = convertView.FindViewById<TextView>(Resource.Id.idTextView);
            var nameTextView = convertView.FindViewById<TextView>(Resource.Id.NameListView);

            var persion = _list[position];

            idTextView.Text = persion.Id.ToString();
            nameTextView.Text = persion.PersonName;

            return convertView;
        }
در مرحله‌ی اول بررسی می‌کنیم که اگر convertView برابر با null بود، آن را مقدار دهی کند. این نکته بسیار مهم است، چرا که ListView برای کارآیی بهتر فقط آن آیتم هایی را که در دید کاربر باشد، با متد GetView لود میکند و دوباره با اسکرول لیست، عمل فراخوانی متد انجام می‌شود؛ البته اینبار بدون مقدار null برای convertView. بنابراین اگر دیدید که هنگام اسکرول لیست، آیتم‌ها جابجا شدند، این بخش از متد را دوباره بررسی نمایید.
Inflate متدی است که Layout و نگه دارنده‌ی  layout را گرفته و آن را برای نمایش در Activity آماده می‌کند. سپس دو View را که در Layout ما وجود دارند، گرفته مقدار دهی می‌کنیم و در آخر هم convertView را برای نمایش به سمت ListView می‌فرستیم.
حال متد OnCreate را به صورت زیر بازنویسی نموده و برنامه را اجرا می‌کنیم.
protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            List<Model.Person> personList = new List<Model.Person>
            {
                new Model.Person() {Id = 1, PersonName = "Mohammad", },
                new Model.Person() {Id = 2, PersonName = "Ali", },
                new Model.Person() {Id = 3, PersonName = "Fatemeh", },
                new Model.Person() {Id = 4, PersonName = "hasan", },
                new Model.Person() {Id = 5, PersonName = "Husein", },
                new Model.Person() {Id = 6, PersonName = "Mohsen", },
                new Model.Person() {Id = 14, PersonName = "Mahdi", },
            };
            var personAdapter = new Adapters.PersonAdapter(this, personList);

            var listview = FindViewById<ListView>(Resource.Id.NameListView);
            listview.Adapter = personAdapter;
        }
اشتراک‌ها
دوره بررسی ASP.NET CORE Identity

ASP.NET CORE Identity Under the Hood | Authentication & Authorization | .NET 8

00:00:00 Introduction
00:04:31 1. Security Overview
00:10:34 2. Authentication and Authorization Flow
00:17:18 3. ASP.NET Core Basics
00:23:31 4. Security Context in ASP.NET Core
00:27:23 5. Anonymous Identity
00:33:26 6. Create a Login Page with Razor Pages
00:46:02 7. Generate Cookie with Cookie Authentication Handler
01:06:01 8. Authenticaiton Middle and Authentication Scheme
01:15:16 9. Authorization Architecture Flow
01:23:58 10. Simple Policy based Authorization
01:43:50 11. Login & Logout Partial View
01:52:56 12. Custom Policy based Authorization
02:05:11 13. Cookie Lifespan & Browser Session 

دوره بررسی ASP.NET CORE Identity
مطالب
Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایه‌ی اعتبارسنجی و احراز هویت
در این قسمت می‌خواهیم پایه‌ی اعتبارسنجی و احراز هویت سمت سرور برنامه‌ی کلاینت Blazor WASM را بر اساس JWT یکپارچه با ASP.NET Core Identity تامین کنیم. اگر با JWT آشنایی ندارید، نیاز است مطالب زیر را در ابتدا مطالعه کنید:
- «معرفی JSON Web Token»

توسعه‌ی IdentityUser

در قسمت‌های 21 تا 23، روش نصب و یکپارچگی ASP.NET Core Identity را با یک برنامه‌ی Blazor Server بررسی کردیم. در پروژه‌ی Web API جاری هم از قصد داریم از ASP.NET Core Identity استفاده کنیم؛ البته بدون نصب UI پیش‌فرض آن. به همین جهت فقط از ApplicationDbContext آن برنامه که از IdentityDbContext مشتق شده و همچنین قسمتی از تنظیمات سرویس‌های ابتدایی آن که در قسمت قبل بررسی کردیم، در اینجا استفاده خواهیم کرد.
IdentityUser پیش‌فرض که معرف موجودیت کاربران یک سیستم مبتنی بر ASP.NET Core Identity است، برای ثبت نام یک کاربر، فقط به ایمیل و کلمه‌ی عبور او نیاز دارد که نمونه‌ای از آن‌را در حین معرفی «ثبت کاربر ادمین Identity» بررسی کردیم. اکنون می‌خواهیم این موجودیت پیش‌فرض را توسعه داده و برای مثال نام کاربر را نیز به آن اضافه کنیم. برای اینکار فایل جدید BlazorServer\BlazorServer.Entities\ApplicationUser .cs را به پروژه‌ی Entities با محتوای زیر اضافه می‌کنیم:
using Microsoft.AspNetCore.Identity;

namespace BlazorServer.Entities
{
    public class ApplicationUser : IdentityUser
    {
        public string Name { get; set; }
    }
}
برای توسعه‌ی IdentityUser پیش‌فرض، فقط کافی است از آن ارث‌بری کرده و خاصیت جدیدی را به خواص موجود آن اضافه کنیم. البته برای شناسایی IdentityUser، نیاز است بسته‌ی نیوگت Microsoft.AspNetCore.Identity.EntityFrameworkCore را نیز در این پروژه نصب کرد.
اکنون که یک ApplicationUser سفارشی را ایجاد کردیم، نیازی نیست تا DbSet خاص آن‌را به ApplicationDbContext برنامه اضافه کنیم. برای معرفی آن به برنامه ابتدا باید به فایل BlazorServer\BlazorServer.DataAccess\ApplicationDbContext.cs مراجعه کرده و نوع IdentityUser را به IdentityDbContext، از طریق آرگومان جنریکی که می‌پذیرد، معرفی کنیم:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
زمانیکه این آرگومان جنریک قید نشود، از همان نوع IdentityUser پیش‌فرض خودش استفاده می‌کند.
پس از این تغییر، در فایل BlazorWasm\BlazorWasm.WebApi\Startup.cs نیز باید ApplicationUser را به عنوان نوع جدید کاربران، معرفی کرد:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
  
            // ...

پس از این تغییرات، باید از طریق خط فرمان به پوشه‌ی BlazorServer.DataAccess وارد شد و دستورات زیر را جهت ایجاد و اعمال Migrations متناظر با تغییرات فوق، اجرا کرد. چون در این دستورات اینبار پروژه‌ی آغازین، به پروژه‌ی Web API اشاره می‌کند، باید بسته‌ی نیوگت Microsoft.EntityFrameworkCore.Design را نیز به پروژه‌ی آغازین اضافه کرد، تا بتوان آن‌ها را با موفقیت به پایان رساند:
dotnet tool update --global dotnet-ef --version 5.0.4
dotnet build
dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddNameToAppUser --context ApplicationDbContext
dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext


ایجاد مدل‌های ثبت نام

در ادامه می‌خواهیم کنترلری را ایجاد کنیم که کار ثبت نام و لاگین را مدیریت می‌کند. برای این منظور باید بتوان از کاربر، اطلاعاتی مانند نام کاربری و کلمه‌ی عبور او را دریافت کرد و پس از پایان عملیات نیز نتیجه‌ی آن‌را بازگشت داد. به همین جهت دو مدل زیر را جهت مدیریت قسمت ثبت نام، به پروژه‌ی BlazorServer.Models اضافه می‌کنیم:
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class UserRequestDTO
    {
        [Required(ErrorMessage = "Name is required")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Email is required")]
        [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
                ErrorMessage = "Invalid email address")]
        public string Email { get; set; }

        public string PhoneNo { get; set; }

        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Required(ErrorMessage = "Confirm password is required")]
        [DataType(DataType.Password)]
        [Compare("Password", ErrorMessage = "Password and confirm password is not matched")]
        public string ConfirmPassword { get; set; }
    }
}
و مدل پاسخ عملیات ثبت نام:
    public class RegistrationResponseDTO
    {
        public bool IsRegistrationSuccessful { get; set; }

        public IEnumerable<string> Errors { get; set; }
    }


ایجاد و تکمیل کنترلر Account، جهت ثبت نام کاربران

در ادامه نیاز داریم تا جهت ارائه‌ی امکانات اعتبارسنجی و احراز هویت کاربران، کنترلر جدید Account را به پروژه‌ی Web API اضافه کنیم:
using System;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Linq;
using BlazorServer.Common;

namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class AccountController : ControllerBase
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public AccountController(SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager)
        {
            _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> SignUp([FromBody] UserRequestDTO userRequestDTO)
        {
            var user = new ApplicationUser
            {
                UserName = userRequestDTO.Email,
                Email = userRequestDTO.Email,
                Name = userRequestDTO.Name,
                PhoneNumber = userRequestDTO.PhoneNo,
                EmailConfirmed = true
            };
            var result = await _userManager.CreateAsync(user, userRequestDTO.Password);
            if (!result.Succeeded)
            {
                var errors = result.Errors.Select(e => e.Description);
                return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
            }

            var roleResult = await _userManager.AddToRoleAsync(user, ConstantRoles.Customer);
            if (!roleResult.Succeeded)
            {
                var errors = result.Errors.Select(e => e.Description);
                return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false });
            }

            return StatusCode(201); // Created
        }
    }
}
- در اینجا اولین اکشن متد کنترلر Account را مشاهده می‌کنید که کار ثبت نام یک کاربر را انجام می‌دهد. نمونه‌‌ای از این کدها پیشتر در قسمت 23 این سری، زمانیکه کاربر جدیدی را با نقش ادمین تعریف کردیم، مشاهده کرده‌اید.
- در تعریف ابتدایی این کنترلر، ویژگی‌های زیر ذکر شده‌اند:
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
می‌خواهیم مسیریابی آن با api/ شروع شود و به صورت خودکار بر اساس نام کنترلر و نام اکشن متدها، تعیین گردد. همچنین نمی‌خواهیم مدام کدهای بررسی معتبر بودن ModelState را در کنترلرها قرار دهیم. به همین جهت از ویژگی ApiController استفاده شده تا اینکار را به صورت خودکار انجام دهد. قرار دادن فیلتر Authorize بر روی یک کنترلر سبب می‌شود تا تمام اکشن متدهای آن به کاربران اعتبارسنجی شده محدود شوند؛ مگر اینکه عکس آن به صورت صریح توسط فیلتر AllowAnonymous مشخص گردد. نمونه‌ی آن‌را در اکشن متد عمومی SignUp در اینجا مشاهده می‌کنید.

تا اینجا اگر برنامه را اجرا کنیم، می‌توان با استفاده از Swagger UI، آن‌را آزمایش کرد:


که با اجرای آن، برای نمونه به خروجی زیر می‌رسیم:


که عنوان می‌کند کلمه‌ی عبور باید حداقل دارای یک عدد و یک حرف بزرگ باشد. پس از اصلاح آن، status-code=201 را دریافت خواهیم کرد.
و اگر سعی کنیم همین کاربر را مجددا ثبت نام کنیم، با خطای زیر مواجه خواهیم شد:



ایجاد مدل‌های ورود به سیستم

در پروژه‌ی Web API، از UI پیش‌فرض ASP.NET Core Identity استفاده نمی‌کنیم. به همین جهت نیاز است مدل‌های قسمت لاگین را به صورت زیر تعریف کنیم:
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class AuthenticationDTO
    {
        [Required(ErrorMessage = "UserName is required")]
        [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$",
            ErrorMessage = "Invalid email address")]
        public string UserName { get; set; }

        [Required(ErrorMessage = "Password is required.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
}
به همراه مدل پاسخ ارائه شده‌ی حاصل از عملیات لاگین:
using System.Collections.Generic;

namespace BlazorServer.Models
{
    public class AuthenticationResponseDTO
    {
        public bool IsAuthSuccessful { get; set; }

        public string ErrorMessage { get; set; }

        public string Token { get; set; }

        public UserDTO UserDTO { get; set; }
    }

    public class UserDTO
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string PhoneNo { get; set; }
    }
}
که در اینجا اگر عملیات لاگین با موفقیت به پایان برسد، یک توکن، به همراه اطلاعاتی از کاربر، به سمت کلاینت ارسال خواهد شد؛ در غیر اینصورت، متن خطای مرتبط بازگشت داده می‌شود.


ایجاد مدل مشخصات تولید JSON Web Token

پس از لاگین موفق، نیاز است یک JWT را تولید کرد و در اختیار کلاینت قرار داد. مشخصات ابتدایی تولید این توکن، توسط مدل زیر تعریف می‌شود:
namespace BlazorServer.Models
{
    public class BearerTokensOptions
    {
        public string Key { set; get; }

        public string Issuer { set; get; }

        public string Audience { set; get; }

        public int AccessTokenExpirationMinutes { set; get; }
    }
}
که شامل کلید امضای توکن، مخاطبین، صادر کننده و مدت زمان اعتبار آن به دقیقه‌است و به صورت زیر در فایل BlazorWasm\BlazorWasm.WebApi\appsettings.json تعریف می‌شود:
{
  "BearerTokens": {
    "Key": "This is my shared key, not so secret, secret!",
    "Issuer": "https://localhost:5001/",
    "Audience": "Any",
    "AccessTokenExpirationMinutes": 20
  }
}
پس از این تعاریف، جهت دسترسی به مقادیر آن توسط سیستم تزریق وابستگی‌ها، مدخل آن‌را به صورت زیر به کلاس آغازین Web API اضافه می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions<BearerTokensOptions>().Bind(Configuration.GetSection("BearerTokens"));
            // ...

ایجاد سرویسی برای تولید JSON Web Token

سرویس زیر به کمک سرویس توکار UserManager مخصوص Identity و مشخصات ابتدایی توکنی که معرفی کردیم، کار تولید یک JWT را انجام می‌دهد:
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using BlazorServer.Entities;
using BlazorServer.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace BlazorServer.Services
{

    public interface ITokenFactoryService
    {
        Task<string> CreateJwtTokensAsync(ApplicationUser user);
    }

    public class TokenFactoryService : ITokenFactoryService
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly BearerTokensOptions _configuration;

        public TokenFactoryService(
                UserManager<ApplicationUser> userManager,
                IOptionsSnapshot<BearerTokensOptions> bearerTokensOptions)
        {
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            if (bearerTokensOptions is null)
            {
                throw new ArgumentNullException(nameof(bearerTokensOptions));
            }
            _configuration = bearerTokensOptions.Value;
        }

        public async Task<string> CreateJwtTokensAsync(ApplicationUser user)
        {
            var signingCredentials = new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Key)),
                SecurityAlgorithms.HmacSha256);
            var claims = await getClaimsAsync(user);
            var now = DateTime.UtcNow;
            var tokenOptions = new JwtSecurityToken(
                issuer: _configuration.Issuer,
                audience: _configuration.Audience,
                claims: claims,
                notBefore: now,
                expires: now.AddMinutes(_configuration.AccessTokenExpirationMinutes),
                signingCredentials: signingCredentials);
            return new JwtSecurityTokenHandler().WriteToken(tokenOptions);
        }

        private async Task<List<Claim>> getClaimsAsync(ApplicationUser user)
        {
            string issuer = _configuration.Issuer;
            var claims = new List<Claim>
            {
                // Issuer
                new Claim(JwtRegisteredClaimNames.Iss, issuer, ClaimValueTypes.String, issuer),
                // Issued at
                new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, issuer),
                new Claim(ClaimTypes.Name, user.Email, ClaimValueTypes.String, issuer),
                new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String, issuer),
                new Claim("Id", user.Id, ClaimValueTypes.String, issuer),
                new Claim("DisplayName", user.Name, ClaimValueTypes.String, issuer),
            };

            var roles = await _userManager.GetRolesAsync(user);
            foreach (var role in roles)
            {
                claims.Add(new Claim(ClaimTypes.Role, role, ClaimValueTypes.String, issuer));
            }
            return claims;
        }
    }
}
کار افزودن نقش‌های یک کاربر به توکن او، به کمک متد userManager.GetRolesAsync انجام شده‌است. نمونه‌ای از این سرویس را پیشتر در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» مشاهده کرده‌اید. البته در آنجا از سیستم Identity برای تامین نقش‌های کاربران استفاده نمی‌شود و مستقل از آن عمل می‌کند.

در آخر، این سرویس را به صورت زیر به لیست سرویس‌های ثبت شده‌ی پروژه‌ی Web API، اضافه می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<ITokenFactoryService, TokenFactoryService>();
            // ...


تکمیل کنترلر Account جهت لاگین کاربران

پس از ثبت نام کاربران، اکنون می‌خواهیم امکان لاگین آن‌ها را نیز فراهم کنیم:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class AccountController : ControllerBase
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly ITokenFactoryService _tokenFactoryService;

        public AccountController(
            SignInManager<ApplicationUser> signInManager,
            UserManager<ApplicationUser> userManager,
            ITokenFactoryService tokenFactoryService)
        {
            _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
            _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
            _tokenFactoryService = tokenFactoryService;
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> SignIn([FromBody] AuthenticationDTO authenticationDTO)
        {
            var result = await _signInManager.PasswordSignInAsync(
                    authenticationDTO.UserName, authenticationDTO.Password,
                    isPersistent: false, lockoutOnFailure: false);
            if (!result.Succeeded)
            {
                return Unauthorized(new AuthenticationResponseDTO
                {
                    IsAuthSuccessful = false,
                    ErrorMessage = "Invalid Authentication"
                });
            }

            var user = await _userManager.FindByNameAsync(authenticationDTO.UserName);
            if (user == null)
            {
                return Unauthorized(new AuthenticationResponseDTO
                {
                    IsAuthSuccessful = false,
                    ErrorMessage = "Invalid Authentication"
                });
            }

            var token = await _tokenFactoryService.CreateJwtTokensAsync(user);
            return Ok(new AuthenticationResponseDTO
            {
                IsAuthSuccessful = true,
                Token = token,
                UserDTO = new UserDTO
                {
                    Name = user.Name,
                    Id = user.Id,
                    Email = user.Email,
                    PhoneNo = user.PhoneNumber
                }
            });
        }
    }
}
در اکشن متد جدید لاگین، اگر عملیات ورود به سیستم با موفقیت انجام شود، با استفاده از سرویس Token Factory که آن‌را پیشتر ایجاد کردیم، توکن مخصوصی را به همراه اطلاعاتی از کاربر، به سمت برنامه‌ی کلاینت بازگشت می‌دهیم.

تا اینجا اگر برنامه را اجرا کنیم، می‌توان در قسمت ورود به سیستم، برای نمونه مشخصات کاربر ادمین را وارد کرد:


و پس از اجرای درخواست، به خروجی زیر می‌رسیم:


که در اینجا JWT تولید شده‌ی به همراه قسمتی از مشخصات کاربر، در خروجی نهایی مشخص است. می‌توان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که به این خروجی می‌رسیم و حاوی claims تعریف شده‌است:
{
  "iss": "https://localhost:5001/",
  "iat": 1616396383,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir",
  "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c",
  "DisplayName": "vahid@dntips.ir",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin",
  "nbf": 1616396383,
  "exp": 1616397583,
  "aud": "Any"
}


تنظیم Web API برای پذیرش و پردازش JWT ها

تا اینجا پس از لاگین، یک JWT را در اختیار کلاینت قرار می‌دهیم. اما اگر کلاینت این JWT را به سمت سرور ارسال کند، اتفاق خاصی رخ نخواهد داد و توسط آن، شیء User قابل دسترسی در یک اکشن متد، به صورت خودکار تشکیل نمی‌شود. برای رفع این مشکل، ابتدا بسته‌ی جدید نیوگت Microsoft.AspNetCore.Authentication.JwtBearer را به پروژه‌ی Web API اضافه می‌کنیم، سپس به کلاس آغازین پروژه‌ی Web API مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            var bearerTokensSection = Configuration.GetSection("BearerTokens");
            services.AddOptions<BearerTokensOptions>().Bind(bearerTokensSection);
            // ...

            var apiSettings = bearerTokensSection.Get<BearerTokensOptions>();
            var key = Encoding.UTF8.GetBytes(apiSettings.Key);
            services.AddAuthentication(opt =>
            {
                opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(cfg =>
            {
                cfg.RequireHttpsMetadata = false;
                cfg.SaveToken = true;
                cfg.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateAudience = true,
                    ValidateIssuer = true,
                    ValidAudience = apiSettings.Audience,
                    ValidIssuer = apiSettings.Issuer,
                    ClockSkew = TimeSpan.Zero,
                    ValidateLifetime = true
                };
            });

            // ...
در اینجا در ابتدا اعتبارسنجی از نوع Jwt تعریف شده‌است و سپس پردازش کننده و وفق دهنده‌ی آن‌را به سیستم اضافه کرده‌ایم تا توکن‌های دریافتی از هدرهای درخواست‌های رسیده را به صورت خودکار پردازش و تبدیل به Claims شیء User یک اکشن متد کند.


افزودن JWT به تنظیمات Swagger

هر کدام از اکشن متدهای کنترلرهای Web API برنامه که مزین به فیلتر Authorize باشد‌، در Swagger UI با یک قفل نمایش داده می‌شود. در این حالت می‌توان این UI را به نحو زیر سفارشی سازی کرد تا بتواند JWT را دریافت و به سمت سرور ارسال کند:
namespace BlazorWasm.WebApi
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            // ...

            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "BlazorWasm.WebApi", Version = "v1" });
                c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
                {
                    In = ParameterLocation.Header,
                    Description = "Please enter the token in the field",
                    Name = "Authorization",
                    Type = SecuritySchemeType.ApiKey
                });
                c.AddSecurityRequirement(new OpenApiSecurityRequirement {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "Bearer"
                            }
                        },
                        new string[] { }
                    }
                });
            });
        }

        // ...


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-25.zip
مطالب
Blazor 5x - قسمت پنجم - مبانی Blazor - بخش 2 - کامپوننت‌ها
انتقال محتوای کامپوننت Index به یک کامپوننت جدید و تعریف مسیریابی و مدخل منوی آن

پیش از ادامه‌ی مثال قسمت قبل، قصد داریم تمام کدهای موجود در فایل Pages\Index.razor را به یک فایل اختصاصی آن‌ها منتقل کرده و مسیریابی و منوی آن‌را تکمیل کنیم. به همین جهت در پوشه‌ی Pages، یک پوشه‌ی جدید را به نام LearnBlazor ایجاد کرده و درون آن، فایل خالی BindProp.razor را ایجاد می‌کنیم. سپس تمام محتوای فایل Pages\Index.razor را cut کرده و به درون فایل جدید Pages\LearnBlazor\BindProp.razor، منتقل و Paste می‌کنیم.
پس از این تغییرات، در فایل Pages\Index.razor، مهم‌ترین سطر آن، همان اولین سطر تعریف مسیریابی آن خواهد بود و هر محتوای دلخواهی که علاقمند بودید:
@page "/"

<h1>Hello, world!</h1>
در ادامه چون می‌خواهیم گزینه‌ی منوی جدیدی را برای BindProp.razor تعریف کنیم، سطر اول آن‌را به صورت زیر تغییر می‌دهیم:
@page "/bindprop"
با اینکار، این کامپوننت صرفنظر از محل قرارگیری آن که اکنون در پوشه‌ی Pages\LearnBlazor است، در مسیر https://localhost:5001/bindprop قابل دسترسی خواهد شد. اما چگونه باید مدخل منوی جدیدی را برای آن تعریف کرد؟ برای اینکار به فایل Shared\NavMenu.razor مراجعه کرده و دقیقا شبیه به ساختار مداخل منوهای Home ، Counter و غیره، مدخل جدیدی را برای آن تعریف می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="bindprop">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Bind Properties
    </NavLink>
</li>
در اینجا برچسب مدخل جدید تعریف شده، Bind Properties است و href لینک به آن، دقیقا به مسیریابی تعریف شده‌ی در فایل BindProp.razor اشاره می‌کند.



نمایش لیست اتاق‌های تعریف شده، به همراه ویژگی‌های آن‌ها

در قسمت قبل، نمایش ردیفی لیست اتاق‌های تعریف شده را مشاهده کردید. در این قسمت می‌خواهیم هر اتاق تعریف شده را در یک card جداگانه نمایش دهیم. هدف این است که در ابتدا به یک UI متداول شلوغ برسیم و بعد شروع کنیم به Refactoring این UI پیچیده، به کامپوننت‌های کوچک‌تر تشکیل دهنده‌ی آن، جهت مدیریت ساده‌تر این UI و درک بهتر آن. بنابراین در ابتدا با یک کامپوننت کلی شلوغ، شروع خواهیم کرد.
به همین جهت فایل جدید Pages\LearnBlazor\DemoHotel.razor را برای نمایش لیست اتاق‌های موجود اضافه می‌کنیم. سپس محتوای آن‌را به صورت زیر تغییر خواهیم داد:
@page "/demoHotel"

<h3>Hotel Rooms</h3>
<div class="border p-2 mt-2" style="background-color:azure">
    <h2 class="text-info">Rooms List</h2>
    <div class="row container">
        @foreach (var room in Rooms)
        {
            <div class="bg-light border p-2 col-5 ml-2">
                <h4 class="text-secondary">Room - @room.Id</h4>

                @room.Name<br />
                @room.Price.ToString("c")<br />
                <input type="checkbox" @bind-value="room.IsActive" checked="@(room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
                <span>This room is @(room.IsActive?"Active": "InActive")</span>

                @if (room.IsActive)
                {
                    @foreach (var roomProp in room.RoomProps)
                    {
                        <p>@roomProp.Name - @roomProp.Value</p>
                    }
                }

                <input type="button" class="btn btn-danger" value="Delete" />
                <input type="button" class="btn btn-success" value="Edit" />
            </div>
        }
    </div>
</div>
- قسمت کدهای آن که در اینجا ذکر نشده (code@)، با قسمت کدهای کامپوننت Pages\LearnBlazor\BindProp.razor که در قسمت قبل تهیه کردیم، یکی است و هدف از آن، ارائه‌ی List<BlazorRoom> Rooms است که در کدهای razor جاری استفاده شده‌است.
- سپس مسیریابی منتهی به این کامپوننت، به آدرس demoHotel/ تنظیم شده‌است. این مسیریابی را در کامپوننت Shared\NavMenu.razor به صورت زیر مورد استفاده قرار خواهیم داد تا مدخل منوی جدیدی برای آن تهیه شود:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="demoHotel">
      <span class="oi oi-list-rich" aria-hidden="true"></span> Demo Hotel
    </NavLink>
</li>
- در این کامپوننت، با ایجاد حلقه‌ای بر روی لیست اتاق‌ها، مشخصات هر کدام نمایش داده می‌شود. همچنین در اینجا اگر اتاق در حال نمایش فعال باشد، لیست خواص آن نیز درج خواهد شد. به علاوه دو دکمه‌ی جدید حذف و ویرایش نیز در انتهای هر برگه اضافه شده‌است:



تبدیل دکمه‌های حذف و ویرایش هر اتاق به یک کامپوننت جدید

اکنون می‌خواهیم کامپوننت شلوغ Pages\LearnBlazor\DemoHotel.razor را به چند زیر کامپوننت بشکنیم تا هر کدام وظایف خاص خود را انجام دهند و در نهایت به یک UI قابل درک‌تر برسیم. برای مثال می‌خواهیم دکمه‌های حذف و ویرایش هر اتاق را به یک کامپوننت جدید منتقل کنیم تا هم این UI خلوت‌تر شود و هم اگر در قسمت دیگری از برنامه نیاز به یک چنین دکمه‌هایی بود، بتوان از آن کامپوننت اختصاصی، استفاده‌ی مجدد کرد.
برای این منظور ابتدا پوشه‌ی جدید Pages\LearnBlazor\LearnBlazor‍Components را افزوده و سپس در داخل آن، فایل جدید کامپوننت EditDeleteButton.razor را نیز ایجاد می‌کنیم. در این فایل جدید در ابتدا کدهای دو دکمه‌ی تعریف شده را از کامپوننت DemoHotel.razor انتخاب و cut کرده و سپس در این فایل جدید paste می‌کنیم. در این کامپوننت جدید، نیازی به تعریف page@ و مسیریابی آن نیست. به این معنا که این کامپوننت، یک کامپوننت اشتراکی است و routable نیست و قرار است در داخل یک کامپوننت دیگر مورد استفاده قرار گیرد.
بنابراین تا اینجا محتوای کامپوننت EditDeleteButton.razor فقط از دو سطر زیر تشکیل می‌شود:
<input type="button" class="btn btn-danger" value="Delete" />
<input type="button" class="btn btn-success" value="Edit" />
در ادامه برای درج این کامپوننت در حلقه‌ی نمایشی آن در کامپوننت DemoHotel، باید به صورت زیر عمل کرد که به فضای نام کامل این کامپوننت اشاره می‌کند:
<BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton></BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton>
برای اینکه مجبور به تعریف یک چنین نام طولانی نباشیم، می‌توان فضای نام پوشه‌ی آن‌را در انتهای فایل Imports.razor_ قرار داد:
@using BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents
البته اگر قرار نیست از این کامپوننت در سایر کامپوننت‌ها استفاده شود و فقط یک محل استفاده را دارد، می‌توان این using را در بالای تعاریف فایل DemoHotel.razor نیز قرار داد.

اکنون می‌توان تعریف مدخل کامپوننت را به صورت زیر خلاصه کرد:
<EditDeleteButton></EditDeleteButton>


ارسال پارامترها به یک کامپوننت

فرض کنید قصد داریم دکمه‌های ویرایش و حذف را تنها به کاربران ادمین نمایش دهیم. به همین جهت نیاز است بتوان پارامتری مانند IsAdmin را به کامپوننت EditDeleteButton ارسال کرد. برای اینکار کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\EditDeleteButton.razor را به صورت زیر ویرایش می‌کنیم:
@if (IsAdmin)
{
    <input type="button" class="btn btn-danger" value="Delete" />
    <input type="button" class="btn btn-success" value="Edit" />
}

@code
{
    [Parameter]
    public bool IsAdmin { get; set; }
}
در اینجا خواص عمومی مزین شده‌ی با ویژگی Parameter، به عنوان پارامتر ورودی کامپوننت عمل می‌کنند. برای نمونه بر اساس مقدار خاصیت IsAdmin، توسط یک if@ تصمیم خواهیم گرفت که آیا قسمتی از UI نمایش داده شود یا خیر؟

پس از تعریف این پارامتر ورودی، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<EditDeleteButton IsAdmin="true"></EditDeleteButton>


انتقال هر اتاق به کامپوننت مجزای خاص خودش

در ادامه می‌خواهیم محتوای حلقه‌ی foreach (var room in Rooms)@ کامپوننت DemoHotel را به طور کامل cut کرده و در یک کامپوننت جدید paste کنیم تا به حلقه‌ای خواناتر و با مسئولیت‌های کمتری برسیم. نگهداری کدهایی که قسمت‌های مختلف آن از هم ایزوله شده‌اند و دامنه‌ی تغییرات آن‌ها کاملا مشخص و محدود است، در طول زمان بسیار ساده‌تر از نگهداری کدهای UI ای در هم تنیده‌است.
به همین جهت ابتدا کامپوننت جدید Pages\LearnBlazor\LearnBlazor‍Components\IndividualRoom.razor را ایجاد می‌کنیم و سپس، هر آنچه داخل حلقه‌ی foreach یاد شده قرار دارد را انتخاب و cut کرده و درون این کامپوننت جدید paste می‌کنیم:
<div class="bg-light border p-2 col-5 offset-1">
    <h4 class="text-secondary">Room - @Room.Id</h4>

    @Room.Name<br />
    @Room.Price.ToString("c")<br />
    <input type="checkbox" @bind-value="Room.IsActive" checked="@(Room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
    <span>This room is @(Room.IsActive?"Active": "InActive")</span>

    @if (Room.IsActive)
    {
        @foreach (var roomProp in Room.RoomProps)
        {
            <p>@roomProp.Name - @roomProp.Value</p>
        }
    }

    <EditDeleteButton IsAdmin="true"></EditDeleteButton>
</div>

@code
{
    [Parameter]
    public BlazorRoom Room { get; set; }
}
در اینجا پس از paste کدهای داخل حلقه، نیاز به یک پارامتر ورودی که همان شیء Room در حال رندر است، خواهد بود. به همین جهت پارامتر آن‌را تعریف کرده و همچنین کدهای موجود را نیز اندکی ویرایش می‌کنیم، تا از نام این پارامتر جدید استفاده کند.

پس از این تغییر، کدهای حلقه‌ی foreach کامپوننت DemoHotel.razor به صورت زیر خلاصه می‌شوند. در اینجا روش ارسال یک شیء را به پارامتر Room نیز مشاهده می‌کنید (البته ذکر @ در اینجا الزامی نیست و می‌شد از روش مقدار دهی "Room="room نیز استفاده کرد):
<div class="row container">
  @foreach (var room in Rooms)
  {
    <IndividualRoom Room="@room"></IndividualRoom>
  }
</div>
در اینجا می‌توان سلسه مراتب کامپوننت‌ها را مشاهده کرد. کامپوننت DemoHotel، کامپوننت IndividualRoom را فراخوانی می‌کند و این کامپوننت نیز کامپوننت EditDeleteButton را مورد استفاده قرار می‌دهد.


یک تمرین: نمایش لیست امکانات رفاهی هتل

پس از نمایش لیست اتاق‌های یک هتل، قصد داریم لیست امکانات رفاهی آن‌را نیز نمایش دهیم:


 مدل این امکانات را به صورت زیر به پوشه‌ی Models برنامه اضافه می‌کنیم:
namespace BlazorServerSample.Models
{
    public class BlazorAmenity
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }
    }
}
از آنجائیکه قصد داریم لیست آن‌ها را در همان کامپوننت DemoHotel.razor نمایش دهیم، این لیست را به صورت زیر تشکیل می‌دهیم:
@code{

    List<BlazorAmenity> AmenitiesList = new List<BlazorAmenity>();
    // ...

    protected override void OnInitialized()
    {
        base.OnInitialized();

        // ...

        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 111,
            Name = "Gym",
            Description = "24x7 gym room is available."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 222,
            Name = "Swimming Pool",
            Description = "Pool room is open from 6am to 10pm."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 333,
            Name = "Free Brakfast",
            Description = "Enjoy free breakfast at out hotel."
        });
    }
}
در ابتدا فیلد List<BlazorAmenity> AmenitiesList جهت دسترسی به لیست امکانات رفاهی تعریف شده و سپس آن‌را در رویدادگردان OnInitialized، مقدار دهی اولیه کرده‌ایم. در مورد این متدهای چرخه‌ی حیات، در قسمت بعدی بیشتر بحث خواهیم کرد.

اکنون برای نمایش تک تک عناصر این لیست، ابتدا یک کامپوننت منحصر به یک BlazorAmenity را به نام Pages\LearnBlazor\LearnBlazor‍Components\IndividualAmenity.razor ایجاد می‌کنیم با این محتوا:
<div class="bg-light border p-2 col-5 offset-1 mt-2">
    <h4 class="text-secondary">Amenity - @Amenity.Id</h4>

    @Amenity.Name<br />
    @Amenity.Description<br />
</div>

@code
{
    [Parameter]
    public BlazorAmenity Amenity { get; set; }
}
این کامپوننت، یک شیء BlazorAmenity را به عنوان پارامتر دریافت کرده و سپس Id، نام و توضیحات آن‌را نمایش می‌دهد.

در آخر پس از تعریف کامپوننت IndividualAmenity.razor، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<div class="col-12 mt-4">
    <h4 class="text-info">Hotel Amenities</h4>
</div>
@foreach (var amenity in AmenitiesList)
{
    <IndividualAmenity Amenity="@amenity"></IndividualAmenity>
}
در اینجا بر روی لیست امکانات، یک حلقه را تشکیل داده و سپس توسط کامپوننت IndividualAmenity، هر کدام از امکانات را جداگانه نمایش داده‌ایم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-05.zip
مطالب
Attribute Routing در ASP.NET MVC 5
Routing مکانیزم مسیریابی ASP.NET MVC است، که یک URI را به یک اکشن متد نگاشت می‌کند. MVC 5 نوع جدیدی از مسیر یابی را پشتیبانی میکند که Attribute Routing یا مسیریابی نشانه ای نام دارد. همانطور که از نامش پیداست، مسیریابی نشانه ای از Attribute‌ها برای این امر استفاده میکند. این روش به شما کنترل بیشتری روی URI‌های اپلیکیشن تان می‌دهد.
مدل قبلی مسیریابی (conventional-routing) هنوز کاملا پشتیبانی می‌شود. در واقع می‌توانید هر دو تکنیک را بعنوان مکمل یکدیگر در یک پروژه استفاده کنید.
در این پست قابلیت‌ها و گزینه‌های اساسی مسیریابی نشانه ای را بررسی میکنیم.
  • چرا مسیریابی نشانه ای؟
  • فعال سازی مسیریابی نشانه ای
  • پارامتر‌های اختیاری URI و مقادیر پیش فرض
  • پیشوند مسیر ها
  • مسیر پیش فرض
  • محدودیت‌های مسیر ها
      • محدودیت‌های سفارشی
  • نام مسیر ها
  • ناحیه‌ها (Areas)


چرا مسیریابی نشانه ای

برای مثال یک وب سایت تجارت آنلاین بهینه شده اجتماعی، می‌تواند مسیرهایی مانند لیست زیر داشته باشد:
  • {productId:int}/{productTitle}
نگاشت می‌شود به: (ProductsController.Show(int id
  • {username}
نگاشت می‌شود به: (ProfilesController.Show(string username
  • {username}/catalogs/{catalogId:int}/{catalogTitle}
نگاشت می‌شود به: (CatalogsController.Show(string username, int catalogId
در نسخه قبلی ASP.NET MVC، قوانین مسیریابی در فایل RouteConfig.cs تعریف می‌شدند، و اشاره به اکشن‌های کنترلرها به نحو زیر انجام می‌شد:
routes.MapRoute(
    name: "ProductPage",
    url: "{productId}/{productTitle}",
    defaults: new { controller = "Products", action = "Show" },
    constraints: new { productId = "\\d+" }
);
هنگامی که قوانین مسیریابی در کنار اکشن متدها تعریف می‌شوند، یعنی در یک فایل سورس و نه در یک کلاس پیکربندی خارجی، درک و فهم نگاشت URI‌ها به اکشن‌ها واضح‌تر و راحت می‌شود. تعریف مسیر قبلی، می‌تواند توسط یک attribute ساده بدین صورت نگاشت شود:
[Route("{productId:int}/{productTitle}")]
public ActionResult Show(int productId) { ... }

فعال سازی Attribute Routing

برای فعال سازی مسیریابی نشانه ای، متد MapMvcAttributeRoutes را هنگام پیکربندی فراخوانی کنید.
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        routes.MapMvcAttributeRoutes();
    }
}
همچنین می‌توانید مدل قبلی مسیریابی را با تکنیک جدید تلفیق کنید.
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapMvcAttributeRoutes();
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

پارامترهای اختیاری URI و مقادیر پیش فرض

می توانید با اضافه کردن یک علامت سوال به پارامترهای مسیریابی، آنها را optional یا اختیاری کنید. برای تعیین مقدار پیش فرض هم از فرمت parameter=value استفاده می‌کنید.
public class BooksController : Controller
{
    // eg: /books
    // eg: /books/1430210079
    [Route("books/{isbn?}")]
    public ActionResult View(string isbn)
    {
        if (!String.IsNullOrEmpty(isbn))
        {
            return View("OneBook", GetBook(isbn));
        }
        return View("AllBooks", GetBooks());
    }
 
    // eg: /books/lang
    // eg: /books/lang/en
    // eg: /books/lang/he
    [Route("books/lang/{lang=en}")]
    public ActionResult ViewByLanguage(string lang)
    {
        return View("OneBook", GetBooksByLanguage(lang));
    }
}
در این مثال، هر دو مسیر books/ و books/1430210079/ به اکشن متد "View" نگاشت می‌شوند، مسیر اول تمام کتاب‌ها را لیست میکند، و مسیر دوم جزئیات کتابی مشخص را لیست می‌کند. هر دو مسیر books/lang/ و books/lang/en/ به یک شکل نگاشت می‌شوند، چرا که مقدار پیش فرض این پارامتر en تعریف شده.



پیشوند مسیرها (Route Prefixes)

برخی اوقات، تمام مسیرها در یک کنترلر با یک پیشوند شروع می‌شوند. بعنوان مثال:
public class ReviewsController : Controller
{
    // eg: /reviews
    [Route("reviews")]
    public ActionResult Index() { ... }
    // eg: /reviews/5
    [Route("reviews/{reviewId}")]
    public ActionResult Show(int reviewId) { ... }
    // eg: /reviews/5/edit
    [Route("reviews/{reviewId}/edit")]
    public ActionResult Edit(int reviewId) { ... }
}
همچنین می‌توانید با استفاده از خاصیت [RoutePrefix] یک پیشوند عمومی برای کل کنترلر تعریف کنید:
[RoutePrefix("reviews")]
public class ReviewsController : Controller
{
    // eg.: /reviews
    [Route]
    public ActionResult Index() { ... }
    // eg.: /reviews/5
    [Route("{reviewId}")]
    public ActionResult Show(int reviewId) { ... }
    // eg.: /reviews/5/edit
    [Route("{reviewId}/edit")]
    public ActionResult Edit(int reviewId) { ... }
}
در صورت لزوم، می‌توانید برای بازنویسی (override) پیشوند مسیرها از کاراکتر ~ استفاده کنید:
[RoutePrefix("reviews")]
public class ReviewsController : Controller
{
    // eg.: /spotlight-review
    [Route("~/spotlight-review")]
    public ActionResult ShowSpotlight() { ... }
 
    ...
}

مسیر پیش فرض

می توانید خاصیت [Route] را روی کنترلر اعمال کنید، تا اکشن متد را بعنوان یک پارامتر بگیرید. این مسیر سپس روی تمام اکشن متدهای این کنترلر اعمال می‌شود، مگر آنکه یک [Route] بخصوص روی اکشن‌ها تعریف شده باشد.
[RoutePrefix("promotions")]
[Route("{action=index}")]
public class ReviewsController : Controller
{
    // eg.: /promotions
    public ActionResult Index() { ... }
 
    // eg.: /promotions/archive
    public ActionResult Archive() { ... }
 
    // eg.: /promotions/new
    public ActionResult New() { ... }
 
    // eg.: /promotions/edit/5
    [Route("edit/{promoId:int}")]
    public ActionResult Edit(int promoId) { ... }
}

محدودیت‌های مسیر ها

با استفاده از Route Constraints می‌توانید نحوه جفت شدن پارامتر‌ها در قالب مسیریابی را محدود و کنترل کنید. فرمت کلی {parameter:constraint} است. بعنوان مثال:
// eg: /users/5
[Route("users/{id:int}"]
public ActionResult GetUserById(int id) { ... }
 
// eg: users/ken
[Route("users/{name}"]
public ActionResult GetUserByName(string name) { ... }
در اینجا، مسیر اول تنها در صورتی انتخاب می‌شود که قسمت id در URI یک مقدار integer باشد. در غیر اینصورت مسیر دوم انتخاب خواهد شد.
جدول زیر constraint‌ها یا محدودیت هایی که پشتیبانی می‌شوند را لیست می‌کند.
 مثال  توضیحات  محدودیت
 {x:alpha}  کاراکترهای الفبای لاتین را تطبیق (match) می‌دهد (a-z, A-Z).  alpha
 {x:bool}  یک مقدار منطقی را تطبیق می‌دهد.  bool
 {x:datetime}  یک مقدار DateTime را تطبیق می‌دهد.  datetime
 {x:decimal}  یک مقدار پولی را تطبیق می‌دهد.  decimal
 {x:double}  یک مقدار اعشاری 64 بیتی را تطبیق می‌دهد.  double
 {x:float}  یک مقدار اعشاری 32 بیتی را تطبیق می‌دهد.  float
 {x:guid}  یک مقدار GUID را تطبیق می‌دهد.  guid
 {x:int}  یک مقدار 32 بیتی integer را تطبیق می‌دهد.  int
 {(x:length(6}
{(x:length(1,20}
 رشته ای با طول تعیین شده را تطبیق می‌دهد.  length
 {x:long}  یک مقدار 64 بیتی integer را تطبیق می‌دهد.  long
 {(x:max(10}  یک مقدار integer با حداکثر مجاز را تطبیق می‌دهد.  max
 {(x:maxlength(10}  رشته ای با حداکثر طول تعیین شده را تطبیق می‌دهد.  maxlength
 {(x:min(10}  مقداری integer با حداقل مقدار تعیین شده را تطبیق می‌دهد.  min
 {(x:minlength(10}  رشته ای با حداقل طول تعیین شده را تطبیق می‌دهد.  minlength
 {(x:range(10,50}  مقداری integer در بازه تعریف شده را تطبیق می‌دهد.  range
 {(${x:regex(^\d{3}-\d{3}-\d{4}  یک عبارت با قاعده را تطبیق می‌دهد.  regex

توجه کنید که بعضی از constraint ها، مانند "min" آرگومان‌ها را در پرانتز دریافت می‌کنند.
می توانید محدودیت‌های متعددی روی یک پارامتر تعریف کنید، که باید با دونقطه جدا شوند. بعنوان مثال:
// eg: /users/5
// but not /users/10000000000 because it is larger than int.MaxValue,
// and not /users/0 because of the min(1) constraint.
[Route("users/{id:int:min(1)}")]
public ActionResult GetUserById(int id) { ... }
مشخص کردن اختیاری بودن پارامتر ها، باید در آخر لیست constraints تعریف شود:
// eg: /greetings/bye
// and /greetings because of the Optional modifier,
// but not /greetings/see-you-tomorrow because of the maxlength(3) constraint.
[Route("greetings/{message:maxlength(3)?}")]
public ActionResult Greet(string message) { ... }

محدودیت‌های سفارشی

با پیاده سازی قرارداد IRouteConstraint می‌توانید محدودیت‌های سفارشی بسازید. بعنوان مثال، constraint زیر یک پارامتر را به لیستی از مقادیر قابل قبول محدود می‌کند:
public class ValuesConstraint : IRouteConstraint
{
    private readonly string[] validOptions;
    public ValuesConstraint(string options)
    {
        validOptions = options.Split('|');
    }
 
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
        }
        return false;
    }
}
قطعه کد زیر نحوه رجیستر کردن این constraint را نشان می‌دهد:
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        var constraintsResolver = new DefaultInlineConstraintResolver();
 
        constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint));
 
        routes.MapMvcAttributeRoutes(constraintsResolver);
    }
}
حالا می‌توانید این محدودیت سفارشی را روی مسیرها اعمال کنید:
public class TemperatureController : Controller
{
    // eg: temp/celsius and /temp/fahrenheit but not /temp/kelvin
    [Route("temp/{scale:values(celsius|fahrenheit)}")]
    public ActionResult Show(string scale)
    {
        return Content("scale is " + scale);
    }
}

نام مسیر ها

می توانید به مسیرها یک نام اختصاص دهید، با این کار تولید URI‌ها هم راحت‌تر می‌شوند. بعنوان مثال برای مسیر زیر:
[Route("menu", Name = "mainmenu")]
public ActionResult MainMenu() { ... }
می‌توانید لینکی با استفاده از Url.RouteUrl تولید کنید:
<a href="@Url.RouteUrl("mainmenu")">Main menu</a>

ناحیه‌ها (Areas)

برای مشخص کردن ناحیه ای که کنترلر به آن تعلق دارد می‌توانید از خاصیت [RouteArea] استفاده کنید. هنگام استفاده از این خاصیت، می‌توانید با خیال راحت کلاس AreaRegistration را از ناحیه مورد نظر حذف کنید.
[RouteArea("Admin")]
[RoutePrefix("menu")]
[Route("{action}")]
public class MenuController : Controller
{
    // eg: /admin/menu/login
    public ActionResult Login() { ... }
 
    // eg: /admin/menu/show-options
    [Route("show-options")]
    public ActionResult Options() { ... }
 
    // eg: /stats
    [Route("~/stats")]
    public ActionResult Stats() { ... }
}
با این کنترلر، فراخوانی تولید لینک زیر، رشته "Admin/menu/show-options/" را بدست میدهد:
Url.Action("Options", "Menu", new { Area = "Admin" })
به منظور تعریف یک پیشوند سفارشی برای یک ناحیه، که با نام خود ناحیه مورد نظر متفاوت است می‌توانید از پارامتر AreaPrefix استفاده کنید. بعنوان مثال:
[RouteArea("BackOffice", AreaPrefix = "back-office")]
اگر از ناحیه‌ها هم بصورت مسیریابی نشانه ای، و هم بصورت متداول (که با کلاس‌های AreaRegistration پیکربندی می‌شوند) استفاده می‌کنید باید مطمئن شوید که رجیستر کردن نواحی اپلیکیشن پس از مسیریابی نشانه ای پیکربندی می‌شود. به هر حال رجیستر کردن ناحیه‌ها پیش از تنظیم مسیرها بصورت متداول باید صورت گیرد. دلیل آن هم مشخص است، برای اینکه درخواست‌های ورودی بدرستی با مسیرهای تعریف شده تطبیق داده شوند، باید ابتدا attribute routes، سپس area registration و در آخر default route رجیستر شوند. بعنوان مثال:
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapMvcAttributeRoutes();
 
    AreaRegistration.RegisterAllAreas();
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

اشتراک‌ها
آموزش Asp.Net Core Web API CRUD با Angular 16

Asp.Net Core Web API CRUD with Angular 16
In this .Net 7 tutorial, we have implemented CRUD operations in asp.net core web api with angular 16 using entity framework core and SQL server. 

آموزش Asp.Net Core Web API CRUD با Angular 16
مطالب
بارگذاری UserControl در WPF به کمک الگوی MVVM
در نرم افزارهای تحت ویندوز روشها و سلیقه‌های متفاوتی برای چینش فرمها ، منو‌ها و دیگر اجزای برنامه وجود دارد. در یک نرم افزار اتوماسیون اداری که فرمهای ورود اطلاعات زیادی دارد فضای کافی برای نمایش همه‌ی فرم‌ها به کاربر نیست. یکی از روش هایی که می‌تواند به کار رود تقسیم قسمت‌های مختلف نرم افزار در View‌های جداگانه است. این کار استفاده‌ی مجدد از قسمت‌های مختلف و نگهداری کد را سهولت می‌بخشد. 

الگوی متداولی که در نرم افزار‌های WPF و Silverlight استفاده می‌شود الگوی MVVM است. (این الگو در جاوااسکریپت هم به سبب Statefull بودن استفاده می‌شود.) قبلا مطالب زیادی در این سایت جهت آموزش و توضیح این الگوی منتشر شده است.
فرض کنید نرم افزار از چند بخش تشکیل شده :

  • صفحه‌ی اصلی
  • منو
  • یک صفحه‌ی خوش آمدگویی
  • صفحه‌ی ورود و نمایش اطلاعات
می توان اجزا و تعریف هر یک از این قسمت‌ها را در یک UserControl قرار داد و در زمان مناسب آن را بارگذاری کرد. 
سوالی که مطرح است بارگذاری UserControl‌ها به کمک الگوی MVVM چگونه است ؟ 
کدهای XAML صفحه‌ی اصلی : 
<Window x:Class="TwoViews.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MVVM Light View Switching"
        d:DesignHeight="300"
        d:DesignWidth="300"
        DataContext="{Binding Main,
                              Source={StaticResource Locator}}"
        ResizeMode="NoResize"
        SizeToContent="WidthAndHeight"
        mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ContentControl Content="{Binding CurrentViewModel}" />

        <DockPanel Grid.Row="1" Margin="5">
            <Button Width="75"
                    Height="23"
                    Command="{Binding SecondViewCommand}"
                    Content="Second View"
                    DockPanel.Dock="Right" />
            <Button Width="75"
                    Height="23"
                    Command="{Binding FirstViewCommand}"
                    Content="First View"
                    DockPanel.Dock="Left" />
        </DockPanel>
    </Grid>
</Window>

2 دکمه در صفحه‌ی اصلی وجود دارد ، یکی از آنها وظیفه‌ی بارگذاری View اول و دیگری وظیفه‌ی بارگذاری View دوم را دارد ،  این دکمه‌ها نقش منو را در یک نرم افزار واقعی به عهده دارند. 
کدهای View-Model گره خورده (به کمک الگوی ViewModolLocator ) به View اصلی  : 
    /// This is our MainViewModel that is tied to the MainWindow via the 
    /// ViewModelLocator class.
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// Static instance of one of the ViewModels.
        /// </summary>
        private static readonly SecondViewModel SecondViewModel = new SecondViewModel();
        /// <summary>
        /// Static instance of one of the ViewModels.
        /// </summary>
        private static readonly FirstViewModel FirstViewModel = new FirstViewModel();
        /// <summary>
        /// The current view.
        /// </summary>
        private ViewModelBase _currentViewModel;
        /// <summary>
        /// Default constructor.  We set the initial view-model to 'FirstViewModel'.
        /// We also associate the commands with their execution actions.
        /// </summary>
        public MainViewModel()
        {
            CurrentViewModel = FirstViewModel;
            FirstViewCommand = new RelayCommand(ExecuteFirstViewCommand);
            SecondViewCommand = new RelayCommand(ExecuteSecondViewCommand);
        }
        /// <summary>
        /// The CurrentView property.  The setter is private since only this 
        /// class can change the view via a command.  If the View is changed,
        /// we need to raise a property changed event (via INPC).
        /// </summary>
        public ViewModelBase CurrentViewModel
        {
            get { return _currentViewModel; }
            set
            {
                if (_currentViewModel == value)
                    return;
                _currentViewModel = value;
                RaisePropertyChanged("CurrentViewModel");
            }
        }
        /// <summary>
        /// Simple property to hold the 'FirstViewCommand' - when executed
        /// it will change the current view to the 'FirstView'
        /// </summary>
        public ICommand FirstViewCommand { get; private set; }
        /// <summary>
        /// Simple property to hold the 'SecondViewCommand' - when executed
        /// it will change the current view to the 'SecondView'
        /// </summary>
        public ICommand SecondViewCommand { get; private set; }
        /// <summary>
        /// Set the CurrentViewModel to 'FirstViewModel'
        /// </summary>
        private void ExecuteFirstViewCommand()
        {
            CurrentViewModel = FirstViewModel;
        }
        /// <summary>
        /// Set the CurrentViewModel to 'SecondViewModel'
        /// </summary>
        private void ExecuteSecondViewCommand()
        {
            CurrentViewModel = SecondViewModel;
        }
    }

این ViewModel از کلاس پایه‌ی چارچوب MVVM Light مشتق شده است.  Command‌ها جهت Handle کردن کلیک دکمه‌ها هستند . نکته‌ی اصلی این ViewModel پراپرتی CurrentViewModel می‌باشد.  این پراپرتی به ویژگی Content کنترل ContentControl مقید (Bind) شده است. با کلیک شدن روی دکمه‌ها View مورد نظر به کاربر نمایش داده می‌شود.
WPF از کجا می‌داند کدام View را به ازای ViewModel خاص render کند ؟ 
در فایل App.xaml یک سری DataTemplate تعریف شده است : 
    <Application.Resources>
        <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
        <!--
            We define the data templates here so we can apply them across the
            entire application.
         
            The data template just says that if our data type is of a particular
            view-model type, then render the appropriate view.  The framework
            takes care of this dynamically.  Note that the DataContext for
            the underlying view is already set at this point, so the
            view (UserControl), doesn't need to have it's DataContext set
            directly.
        -->
        <DataTemplate DataType="{x:Type vm:SecondViewModel}">
            <views:SecondView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:FirstViewModel}">
            <views:FirstView />
        </DataTemplate>
    </Application.Resources>

به کمک این DataTemplate‌ها مشخص شده اگر نوع داده‌ی ما از یک نوع View-Model خاص می‌باشد View مناسب را به ازای آن Render کند. با تعریف DataTemplate‌ها در App.Xaml می‌توان از آنها در سطح نرم افزار استفاده کرد. می‌توان DataTemplate‌ها را جهت خلوت کردن App.xaml به Resource دیگری انتقال داد.
دریافت مثال : TwoViews.zip 

 
اشتراک‌ها
دیگر سرویس پکی برای SQL Server ارائه نخواهد شد

- SPs will no longer be made available. Only CUs, and GDRs when needed.
- CUs will be delivered more often at first and then less frequently. Every month for the first 12 months, and every quarter for the remainder 4 years of the full 5-year mainstream lifecycle.
- CUs are delivered on the same week of the month: week of 3rd Tuesday.

دیگر سرویس پکی برای SQL Server ارائه نخواهد شد