مطالب
Delegate در سی شارپ
یک Delegate نوعی اشاره‌گر است به توابع در سی شارپ که می‌تواند ارجاعی را به یک یا چند تابع بخصوص داشته باشد. منظور از توابع در سی شارپ، متدها هستند. امضای یک Delegate باید با متدی که به آن اشاره می‌کنید یکی باشد.
using System;
using System.Windows.Forms;
 
namespace CSharpDelegates
{
    public delegate void Display(string sMsg);
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            Display del = new Display(ShowMessage);
            del("This is an example for delegate");
        }
 
        private void ShowMessage(string strMessage)
        {
            MessageBox.Show(strMessage);
        }
 
    }
}
  همانطور که در کد بالا مشاهده می‌کنید، Delegate‌ها بسیار شبیه به کلاس‌ها هستند. می‌توانیم از آنها یک شیء ساخته و نام متدی را که قرار است به آن اشاره کند، از طریق سازنده به آن ارسال کنیم. در کد بالا یک Delegate را با نام Display ساخته‌ایم که به متد ShowMessage اشاره می‌کند. اگر به Delegate و متد ShowMessage دقت کنید خواهید دید که هر دو دارای پارامتر ورودی و امضای یکسانی هستند. ما شیءای به نام Display را از نوع Delegate ساخته‌ایم که متدی به نام ShowMessage را با پارامتر ورودی از نوع string، اجرا می‌کند.
شاید بپرسید که چرا باید از Delegate استفاده کنیم؟ چرا متد ShowMessage را مستقیما اجرا نکنیم؟
خوب، Delegate‌ها برای طراحی فریم ورکهایی با قابلیت استفاده‌ی مجدد از کدهای آنها، بسیار مناسب هستند. بگذارید این مطلب را با یک مثال ساده از کلاس Employee توضیح دهیم.
ویژال استودیو را باز کنید و یک پروژه‌ی Windows Forms Application ساده را با نام CSharpDelegates بسازید. سپس کلاس زیر را به آن اضافه کنید:   
using System.Collections.Generic;
 
namespace CSharpDelegates
{
    public class Employee
    {
        public int EmployeeId { get; set; }
 
        public string Name { get; set; }
 
        public int Experience { get; set; }
 
        public double Salary { get; set; }
 
        public void IncreaseSalary(List<Employee> Employees)
        {
            foreach (Employee emp in Employees)
            {
                if (emp.Salary < 10000)
                {
                    emp.Salary = emp.Salary + emp.Salary * 0.3;
                }
            }
        }
    }
}
در کلاس Employee بالا، تعدادی فیلد و یک متد با نام IncreaseSalary داریم که وظیفه‌ی آن افزایش 30% حقوق کارمندانی است که کمتر از 10000 می‌گیرند. اگر در آینده قصد داشته باشیم که علاوه بر این افزایش حقوق، منطق دیگری را با میزان ترفیع و شایستگی کارمندان نیز لحاظ کنیم، لازم است کدهای متد IncreaseSalary را تغییر دهیم که این کار، یک کار خسته کننده است و شاید ما دوست نداشته باشیم تا کدهای کلاس پایه‌ی Employee را تغییر دهیم. در این نوع سناریوها می‌توان با استفاده از Delegateها، منطق افزایش حقوق و منطق ترفیع و شایستگی کارمندان را از هم جدا کرد. خوب، اولین کار، ویرایش متد IncreaseSalary است:  
using System.Collections.Generic;
 
namespace CSharpDelegates
{
    public delegate bool SalaryIncreaseEligibility(Employee emp);
    public class Employee
    {
        public int EmployeeId { get; set; }
 
        public string Name { get; set; }
 
        public int Experience { get; set; }
 
        public double Salary { get; set; }
 
        public string IncreaseSalary(List<Employee> Employees, SalaryIncreaseEligibility del)
        {
            string sSalIncreasdEmployees = "Salary increased for ";
            foreach (Employee emp in Employees)
            {
                if (del(emp))
                {
                    emp.Salary = emp.Salary + emp.Salary * 0.3;
                    sSalIncreasdEmployees = sSalIncreasdEmployees + emp.Name + " ,";
                }
            }
 
            return sSalIncreasdEmployees;
        }
    }
}
همانطور که در کد بالا قابل مشاهده است، منطق افزایش حقوق بر اساس ترفیع و شایستگی کارمندان را با Delegate ایی به نام SalaryIncreaseEligibility جدا کرده‌ایم. بدین وسیله می‌توانیم منطق شناسایی کردن کارمندان لایق افزایش حقوق را بدون ایجاد تغییری در کلاس Employee سفارشی کنیم. حال بگذارید متد IncreaseSalary از کلاس Employee را با منطق سفارشی خود برای افزایش حقوق کارمندان لایق، با کمک Delegate ایی به نام SalaryIncreaseEligibility اجرا کنیم. 
using System;
using System.Collections.Generic;
using System.Windows.Forms;
 
namespace CSharpDelegates
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            List<Employee> empList = new List<Employee>();
            empList.Add(new Employee() { EmployeeId = 100, Name = "Mark", Salary = 2000, Experience = 3 });
            empList.Add(new Employee() { EmployeeId = 101, Name = "John", Salary = 15000, Experience = 8 });
            empList.Add(new Employee() { EmployeeId = 102, Name = "David", Salary = 4000, Experience = 4 });
            empList.Add(new Employee() { EmployeeId = 103, Name = "Bob", Salary = 50000, Experience = 14 });
            empList.Add(new Employee() { EmployeeId = 104, Name = "Alex", Salary = 9000, Experience = 6 });
 
            SalaryIncreaseEligibility del = new SalaryIncreaseEligibility(SalaryEligibility);
 
            Employee objEmp = new Employee();
            string sMsg = objEmp.IncreaseSalary(empList, del);
 
            MessageBox.Show(sMsg);
        }
 
        private bool SalaryEligibility(Employee emp)
        {
            if (emp.Salary > 10000)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
 
    }
}
در کد بالا ما منطق ترفیع و شایستگی کارمندان را از متد SalaryEligibility جدا کرده‌ایم و این منطق را به کمک Delegate ای به نام SalaryIncreaseEligibility به متد ذکر شده پاس داده‌ایم. در آینده اگر قصد داشته باشیم تا این افزایش حقوق را بر اساس منطق دیگری تعریف کنیم، فقط کافیست که متد SalaryEligibility را تغییر دهیم و دیگر لازم نیست تغییری در کلاس Employee ایجاد کنیم.
مطالب
RadioButtonList در ASP.NET MVC

برای تهیه یک RadioButtonList نیز می‌توان از همان نکته‌ی CheckBoxList استفاده کرد: نام عناصر radio button اضافه شده به صفحه را یکسان وارد می‌کنیم. به این ترتیب یک گروه تشکیل خواهد شد و زمانیکه اطلاعات این عناصر به سرور ارسال می‌شود، اینبار بجای یک آرایه، تنها مقدار کنترل انتخاب شده، ارسال می‌گردد. یک مثال:
یک پروژه جدید و خالی ASP.NET MVC را آغاز کنید. سپس کنترلر Home و View خالی Index را نیز ایجاد نمائید. محتویات این دو را به نحو زیر تغییر دهید:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm1 (Normal)</legend>
@using (Html.BeginForm(actionName: "HandleForm1", controllerName: "Home"))
{
@:your favorite tech: <br />
@Html.RadioButton(name: "tech", value: ".NET", isChecked: true) @:DOTNET <br />
@Html.RadioButton(name: "tech", value: "JAVA", isChecked: false) @:JAVA <br />
@Html.RadioButton(name: "tech", value: "PHP", isChecked: false) @:PHP <br />
<input type="submit" value="Submit" />
}
</fieldset>

using System.Collections.Generic;
using System.Web.Mvc;

namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}

[HttpPost]
public ActionResult HandleForm1(string tech)
{
return RedirectToAction("Index");
}
}
}

در اینجا سه RadioButton با نامی یکسان در صفحه اضافه شده‌اند. سپس داخل متد HandleForm1 یک breakpoint قرار دهید. اکنون برنامه را اجرا کنید و فرم را به سرور ارسال نمائید. پارامتر tech با value عنصر انتخابی مقدار دهی خواهد شد.

تهیه یک RadioButtonList عمومی

اطلاعات فوق را می‌توان تبدیل به یک HtmlHelper با قابلیت استفاده مجدد نیز نمود:

@helper RadioButtonList(string groupName, IEnumerable<System.Web.Mvc.SelectListItem> items)
{
<div class="RadioButtonList">
@foreach (var item in items)
{
@item.Text
<input type="radio" name="@groupName"
value="@item.Value"
@if (item.Selected) { <text>checked="checked"</text> }
/>
<br />
}
</div>
}

برای مثال یک فایل را در مسیر app_code\Helpers.cshtml ایجاد کرده و اطلاعات فوق را به آن اضافه نمائید.
اینبار برای استفاده از آن خواهیم داشت:

using System.Collections.Generic;
using System.Web.Mvc;

namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
ViewBag.Tags = new[]
{
new SelectListItem { Text = ".NET", Value = "Val1", Selected = true },
new SelectListItem { Text = "JAVA", Value = "Val2", Selected = false },
new SelectListItem { Text = "PHP", Value = "Val3", Selected = false }
};
return View();
}

[HttpPost]
public ActionResult HandleForm2(string preferredTechnology)
{
return RedirectToAction("Index");
}
}
}

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>

<fieldset>
<legend>HandleForm2 (Helper)</legend>
@using (Html.BeginForm(actionName: "HandleForm2", controllerName: "Home"))
{
@:your favorite tech: <br />
@Helpers.RadioButtonList("preferredTechnology", (SelectListItem[])ViewBag.Tags)
<input type="submit" value="Submit" />
}
</fieldset>

متد سفارشی تهیه شده، یک آرایه از SelectListItem ها را دریافت کرده و به صورت خودکار تبدیل به RadioButtonList می‌کند. بر اساس نام آن می‌توان به مقدار انتخاب شده ارسالی به سرور در کنترلر مرتبط، دسترسی یافت.


تهیه یک Templated helper سفارشی

در عمل زمانیکه با مدل‌ها کار می‌کنیم و اطلاعات برنامه قرار است Strongly typed باشند، مرسوم است لیستی از انتخاب‌ها را به صورت یک enum تعریف کنند. برای مثال مدل زیر را به برنامه اضافه کنید:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication23.Models
{
public enum Gender
{
[Display(Name = "مرد")]
Male,
[Display(Name = "زن")]
Female,
}

public class User
{
[ScaffoldColumn(false)]
public int Id { set; get; }

[Display(Name = "نام")]
public string Name { set; get; }

[Display(Name = "جنسیت")]
[UIHint("EnumRadioButtonList")]
public Gender Gender { set; get; }
}
}

قصد داریم یک Templated helper سفارشی را به نام EnumRadioButtonList، ایجاد کنیم تا در زمان فراخوانی متد Html.EditorForModel، به صورت خودکار enum تعریف شده را به صورت یک RadioButtonList نمایش دهد.
برای این منظور فایل جدید Views\Shared\EditorTemplates\EnumRadioButtonList.cshtml را به برنامه اضافه کنید. محتوای آن‌را به نحو زیر تغییر دهید:

@using System.ComponentModel.DataAnnotations
@using System.Globalization
@model Enum
@{
Func<Enum, string> getDescription = enumItem =>
{
var type = enumItem.GetType();
var memInfo = type.GetMember(enumItem.ToString());
if (memInfo != null && memInfo.Any())
{
var attrs = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
if (attrs != null && attrs.Any())
return ((DisplayAttribute)attrs[0]).GetName();
}
return enumItem.ToString();
};

var listItems = Enum.GetValues(Model.GetType())
.OfType<Enum>()
.Select(enumItem =>
new SelectListItem()
{
Text = getDescription(enumItem),
Value = enumItem.ToString(),
Selected = enumItem.Equals(Model)
});

string prefix = ViewData.TemplateInfo.HtmlFieldPrefix;
ViewData.TemplateInfo.HtmlFieldPrefix = string.Empty;

int index = 0;
foreach (var li in listItems)
{
string fieldName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", prefix, index++);
<div class="editor-radio">
@Html.RadioButton(prefix, li.Value, li.Selected, new { @id = fieldName })
@Html.Label(fieldName, li.Text)
</div>
}

ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
}

در اینجا به کمک Reflection به اطلاعات enum دریافتی دسترسی خواهیم داشت. بر این اساس می‌توان نام عناصر آن‌را یافت و تبدیل به یک RadioButtonList کرد. البته کار به همینجا ختم نمی‌شود. در این بین باید دقت داشت که ممکن است از ویژگی Display (مانند مدل نمونه فوق) بر روی تک تک عناصر یک enum نیز استفاده شود. به همین جهت این مورد نیز باید پردازش گردد.
نهایتا برای استفاده از این Templated helper سفارشی، کنترلر و View برنامه را به نحو زیر می‌توان تغییر داد:

using System.Collections.Generic;
using System.Web.Mvc;
using MvcApplication23.Models;

namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
var user = new User { Id = 1, Name = "name 1", Gender = Gender.Male };
return View(user);
}

[HttpPost]
public ActionResult HandleForm3(User user)
{
return RedirectToAction("Index");
}
}
}

@model MvcApplication23.Models.User
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm3 (EditorForModel)</legend>
@using (Html.BeginForm(actionName: "HandleForm3", controllerName: "Home"))
{
@Html.EditorForModel()
<input type="submit" value="Submit" />
}
</fieldset>

برای استفاده از یک templated helper سفارشی چندین روش وجود دارد:
الف) همانند مثال فوق از ویژگی UIHint استفاده شود.
ب) نام فایل را به enum.cshtml تغییر دهیم. به این ترتیب از این پس کلیه enumها در صورت استفاده از متد Html.EditorForModel، به صورت خودکار تبدیل به یک RadioButtonList می‌شوند.
ج) متد زیر نیز همین کار را انجام می‌دهد:
@Html.EditorFor(model => model.EnumProperty, "EnumRadioButtonList")


مطالب
React 16x - قسمت 13 - طراحی یک گرید - بخش 3 - مرتب سازی اطلاعات
تا اینجا صفحه بندی و فیلتر کردن اطلاعات را پیاده سازی کردیم. در این قسمت شروع به refactoring کامپوننت movies کرده، جدول آن‌را تبدیل به یک کامپوننت مجزا می‌کنیم و سپس مرتب سازی اطلاعات را نیز به آن اضافه خواهیم کرد.


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

در طراحی فعلی کامپوننت movies، مشکل کوچکی وجود دارد: این کامپوننت تا اینجا، ترکیبی شده‌است از دو کامپوننت صفحه بندی و نمایش لیست گروه‌ها، به همراه جزئیات کامل یک جدول بسیار طولانی. به این مشکل، mixed levels of abstractions می‌گویند. در اینجا دو کامپوننت سطح بالا را داریم، به همراه یک جدول سطح پایین که تمام مشخصات آن در معرض دید هستند و با هم مخلوط شده‌اند. یک چنین کدی، یکدست به نظر نمی‌رسد. به همین جهت اولین کاری را که در ادامه انجام خواهیم داد، تعریف یک کامپوننت جدید و انتقال تمام جزئیات جدول نمایش ردیف‌های فیلم‌ها، به آن است. برای این منظور فایل جدید src\components\moviesTable.jsx را ایجاد کرده و توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت MoviesTable را تولید می‌کنیم. این کامپوننت را در پوشه‌ی common قرار ندادیم؛ از این جهت که قابلیت استفاده‌ی مجدد در سایر برنامه‌ها را ندارد. کار آن تنها مرتبط و مختص به اشیاء فیلمی است که در سرویس‌های برنامه داریم. البته در ادامه، این جدول را نیز به چندین کامپوننت با قابلیت استفاده‌ی مجدد، خواهیم شکست؛ اما فعلا در اینجا با اصل کدهای سطح پایین جدول نمایش داده شده‌ی در کامپوننت movies، شروع می‌کنیم، آن‌ها را cut کرده و به متد رندر کامپوننت جدید MoviesTable منتقل می‌کنیم.

پس از انتقال کامل تگ table از کامپوننت movies به داخل متد رندر کامپوننت MoviesTable، در ابتدای آن توسط Object Destructuring، یک آرایه و دو رخ‌د‌‌ادی را که برای مقدار دهی قسمت‌های مختلف آن نیاز داریم، از props فرضی، استخراج می‌کنیم. اینکار کمک می‌کند تا بتوان اینترفیس این کامپوننت را به خوبی مشخص و طراحی کرد:
class MoviesTable extends Component {
  render() {
    const { movies, onDelete, onLike } = this.props;

پس از تعریف متغیرهای مورد نیاز، ابتدا برای اینکه بتوانیم در اینجا نیز مجددا از کامپوننت Like استفاده کنیم، کلاس آن‌را از ماژول مرتبط import می‌کنیم:
import Like from "./common/like";
سپس از onLike تعریف شده، بجای this.handleLike قبلی استفاده می‌کنیم:
// ...
<Like liked={movie.liked} onClick={() => onLike(movie)} />

همچنین در جائیکه onClick دکمه‌ی حذف به this.handleDelete کامپوننت movies متصل بود، از onDelete تعریف شده‌ی در ابتدای متد رندر فوق استفاده خواهیم کرد:
<button
  onClick={() => onDelete(movie)}
  className="btn btn-danger btn-sm"
>
  Delete
</button>

همین اندازه تغییر، این کامپوننت جدید را مجددا قابل استفاده می‌کند. بنابراین به کامپوننت movies بازگشته و ابتدا کلاس آن‌را import می‌کنیم:
import MoviesTable from "./moviesTable";
و سپس المان آن‌را در محل قبلی جدول درج شده، تعریف می‌کنیم:
<MoviesTable
  movies={movies}
  onDelete={this.handleDelete}
  onLike={this.handleLike}
/>
همانطور که مشاهده می‌کنید، ویژگی‌های تعریف شده‌ی در اینجا همان‌هایی هستند که با استفاده از Object Destructuring در ابتدای متد رندر کامپوننت MoviesTable، تعریف کردیم.
پس از این تغییرات، متد رندر کامپوننت movies چنین شکلی را پیدا کرده‌است که در آن سه کامپوننت سطح بالا درج شده‌اند و در یک سطح از abstraction قرار دارند و دیگر مخلوطی از المان‌های سطح بالا و سطح پایین را نداریم:
    return (
      <div className="row">
        <div className="col-3">
          <ListGroup
            items={this.state.genres}
            onItemSelect={this.handleGenreSelect}
            selectedItem={this.state.selectedGenre}
          />
        </div>
        <div className="col">
          <p>Showing {totalCount} movies in the database.</p>
          <MoviesTable
            movies={movies}
            onDelete={this.handleDelete}
            onLike={this.handleLike}
          />
          <Pagination
            itemsCount={totalCount}
            pageSize={this.state.pageSize}
            onPageChange={this.handlePageChange}
            currentPage={this.state.currentPage}
          />
        </div>
      </div>
    );


صدور رخ‌داد مرتب سازی اطلاعات

اکنون نوبت فعالسازی کلیک بر روی سرستون‌های جدول نمایش داده شده و مرتب سازی اطلاعات جدول بر اساس ستون انتخابی است. به همین جهت در کامپوننت MoviesTable، رویداد onSort را هم به لیستی از خواصی که از props انتظار داریم، اضافه می‌کنیم که در نهایت در کامپوننت movies، به یک متد رویدادگردان متصل می‌شود:
class MoviesTable extends Component {
  render() {
    const { movies, onDelete, onLike, onSort } = this.props;

سپس رویداد کلیک بر روی هر سر ستون را توسط onSort و نام خاصیتی که به آن ارسال می‌شود، به استفاده کننده‌ی از کامپوننت MoviesTable منتقل می‌کنیم تا بر اساس نام این خاصیت، کار مرتب سازی اطلاعات را انجام دهد:
    return (
      <table className="table">
        <thead>
          <tr>
            <th style={{ cursor: "pointer" }} onClick={() => onSort("title")}>Title</th>
            <th style={{ cursor: "pointer" }} onClick={() => onSort("genre.name")}>Genre</th>
            <th style={{ cursor: "pointer" }} onClick={() => onSort("numberInStock")}>Stock</th>
            <th style={{ cursor: "pointer" }} onClick={() => onSort("dailyRentalRate")}>Rate</th>
            <th />
            <th />
          </tr>
        </thead>

در ادامه به کامپوننت movies مراجعه کرده و رویداد onSort را مدیریت می‌کنیم. برای این منظور ویژگی جدید onSort را به المان MoviesTable اضافه کرده و آن‌را به متد handleSort متصل می‌کنیم:
<MoviesTable
  movies={movies}
  onDelete={this.handleDelete}
  onLike={this.handleLike}
  onSort={this.handleSort}
/>
متد handleSort هم به صورت زیر تعریف می‌شود:
  handleSort = column => {
    console.log("handleSort", column);
  };


پیاده سازی مرتب سازی اطلاعات

تا اینجا اگر دقت کرده باشید، هر زمانیکه شماره صفحه‌ای تغییر می‌کند یا گروه فیلم خاصی انتخاب می‌شود، ابتدا state را به روز رسانی می‌کنیم که در نتیجه‌ی آن، کار رندر مجدد کامپوننت در DOM مجازی React صورت می‌گیرد. سپس در متد رندر، کار تغییر اطلاعات آرایه‌ی فیلم‌ها را جهت نمایش به کاربر، انجام می‌دهیم.
بنابراین ابتدا در متد رویدادگران handleSort، با فراخوانی متد setState، مقدار path دریافتی حاصل از کلیک بر روی یک سرستون را به همراه صعودی و یا نزولی بودن مرتب سازی، در state کامپوننت جاری تغییر می‌دهیم:
  handleSort = path => {
    console.log("handleSort", path);
    this.setState({ sortColumn: { path, order: "asc" } });
  };
البته بهتر است این sortColumn تعریف شده‌ی در اینجا را به تعریف خاصیت state نیز به صورت مستقیم اضافه کنیم تا در اولین بار نمایش صفحه، تعریف شده و قابل دسترسی باشد:
class Movies extends Component {
  state = {
    // ...
    sortColumn: { path:"title", order: "asc" }
  };

سپس متد getPagedData را که در قسمت قبل اضافه و تکمیل کردیم، جهت اعمال این خواص به روز رسانی می‌کنیم:
  getPagedData() {
    const {
      pageSize,
      currentPage,
      selectedGenre,
      movies: allMovies,
      sortColumn
    } = this.state;

    let filteredMovies =
      selectedGenre && selectedGenre._id
        ? allMovies.filter(m => m.genre._id === selectedGenre._id)
        : allMovies;

    filteredMovies = filteredMovies.sort((movie1, movie2) =>
      movie1[sortColumn.path] > movie2[sortColumn.path]
        ? sortColumn.order === "asc"
          ? 1
          : -1
        : movie2[sortColumn.path] > movie1[sortColumn.path]
        ? sortColumn.order === "asc"
          ? -1
          : 1
        : 0
    );

    const first = (currentPage - 1) * pageSize;
    const last = first + pageSize;
    const pagedMovies = filteredMovies.slice(first, last);

    return { totalCount: filteredMovies.length, data: pagedMovies };
  }
در اینجا کار sort بر اساس sortColumn.path و sortColumn.order پس از فیلتر شدن اطلاعات و پیش از صفحه بندی، انجام می‌شود. در مورد متد sort و filter و امثال آن می‌توانید به مطلب «بررسی معادل‌های LINQ در TypeScript» برای مطالعه‌ی بیشتر مراجعه کنید.

همچنین می‌خواهیم اگر با کلیک بر روی ستونی، روش و جهت مرتب سازی آن صعودی بود، نزولی شود و یا برعکس که یک روش پیاده سازی آن‌را در اینجا مشاهده می‌کنید:
  handleSort = path => {
    console.log("handleSort", path);
    const sortColumn = { ...this.state.sortColumn };
    if (sortColumn.path === path) {
      sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
    } else {
      sortColumn.path = path;
      sortColumn.order = "asc";
    }
    this.setState({ sortColumn });
  };
چون می‌خواهیم خواص this.state.sortColumn را تغییر دهیم و تغییر مستقیم state در React مجاز نیست، ابتدا یک clone از آن‌را ایجاد کرده و سپس بر روی این clone کار می‌کنیم. در نهایت این شیء جدید را بجای شیء قبلی در state به روز رسانی خواهیم کرد.


بهبود کیفیت کدهای مرتب سازی اطلاعات

اگر قرار باشد کامپوننت MoviesTable را در جای دیگری مورد استفاده‌ی مجدد قرار دهیم، زمانیکه این جدول سبب صدور رخ‌دادی می‌شود، باید منطقی را که در متد handleSort فوق مشاهده می‌کنید، مجددا به همین شکل تکرار کنیم. بنابراین این منطق متعلق به کامپوننت MoviesTable است و زمانیکه onSort را فراخوانی می‌کند، بهتر است بجای ارسال path یا همان نام فیلدی که قرار است مرتب سازی بر اساس آن انجام شود، شیء sortColumn را به عنوان خروجی بازگشت دهد. به همین جهت، این منطق را به کلاس MoviesTable منتقل می‌کنیم:
class MoviesTable extends Component {
  raiseSort = path => {
    console.log("raiseSort", path);
    const sortColumn = { ...this.props.sortColumn };
    if (sortColumn.path === path) {
      sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc";
    } else {
      sortColumn.path = path;
      sortColumn.order = "asc";
    }
    this.props.onSort(sortColumn);
  };
در این متد جدید بجای this.state.sortColumn قبلی، اینبار sortColumn را از props دریافت می‌کنیم. بنابراین نیاز خواهد بود تا ویژگی جدید sortColumn را به تعریف المان MoviesTable در کامپوننت movies، اضافه کنیم:
<MoviesTable
  movies={movies}
  onDelete={this.handleDelete}
  onLike={this.handleLike}
  onSort={this.handleSort}
  sortColumn={this.state.sortColumn}
/>

 همچنین در کامپوننت MoviesTable، کار فراخوانی onSort را جهت بازگشت sortColumn محاسبه شده در همین متد raiseSort انجام می‌دهیم. بنابراین تمام onSortهای هدر جدول به this.raiseSort تغییر می‌کنند:
    return (
      <table className="table">
        <thead>
          <tr>
            <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("title")}>Title</th>
            <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("genre.name")}>Genre</th>
            <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("numberInStock")}>Stock</th>
            <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("dailyRentalRate")}>Rate</th>
            <th />
            <th />
          </tr>
        </thead>


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-13.zip
اشتراک‌ها
افزونه NET MAUI Community Toolkit C# Markup Extensions

اگر علاقه ای به توسعه برنامه هایی که با Net MAUI. نوشته خواهند شد با استفاده از XAML ندارید به کمک این افزونه میتوانید همان دستورات را به زبان #C بنویسید.

using System;
using CommunityToolkit.Maui.Markup;
using Microsoft.Maui;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Essentials;
using static CommunityToolkit.Maui.Markup.GridRowsColumns;

namespace HelloMauiMarkup;

class MainPage : ContentPge
{
    public MainPage()
    {
        BindingContext = new MainViewModel();

        Content = new Grid
        {
            RowSpacing = 25,
            ColumnSpacing = 0,

            Padding = Device.RuntimePlatform switch
            {
                Device.iOS => new Thickness(30, 60, 30, 30),
                _ => new Thickness(30)
            },

            RowDefinitions = Rows.Define(
                (Row.HelloWorld, 44),
                (Row.Welcome, Auto),
                (Row.Count, Auto),
                (Row.ClickMeButton, Auto),
                (Row.Image, Star)),

            ColumnDefinitions = Columns.Define(
            (Column.Text, Star),
                (Column.Number, Star)),

            Children =
            {
                new Label { Text = "Hello World" }
.Row(Row.HelloWorld).ColumnSpan(All<Column>())
.Font(size: 32)
.CenterHorizontal().TextCenter(),

                new Label { Text = "Welcome to .NET MAUI Markup Community Toolkit Sample" }
                .Row(Row.Welcome).ColumnSpan(All<Column>())
                    .Font(size: 18)
                .CenterHorizontal().TextCenter(),

new Label { Text = "Current Count: " }
                .Row(Row.Count).Column(Column.Text)
.Font(bold: true)
.End().TextEnd(),

new Label()
                .Row(Row.Count).Column(Column.Number)
                    .Font(bold: true)
                    .Start().TextStart()
                    .Bind<Label, int, string>(Label.TextProperty, nameof(MainViewModel.ClickCount), convert: count => count.ToString())

                new Button { Text = "Click Me" }
                .Row(Row.ClickMeButton)
                .Font(bold: true)
                .CenterHorizontal()
.BindCommand(nameof(ViewModel.ClickMeButtonCommand)),

                new Image { Source = "dotnet_bot.png", WidthRequest = 250, HeightRequest = 310 }
.Row(Row.Image).ColumnSpan(All<Column>())
.CenterHorizontal()
}
};
    }

    enum Row { HelloWorld, Welcome, Count, ClickMeButton, Image }
    enum Column { Text, Number }
}


افزونه NET MAUI Community Toolkit C# Markup Extensions
مطالب
افزودن تصدیق ایمیل به ASP.NET Identity در MVC 5
در پست قبلی نحوه سفارشی کردن پروفایل کاربران در ASP.NET Identity را مرور کردیم. اگر بیاد داشته باشید یک فیلد آدرس ایمیل به کلاس کاربر اضافه کردیم. در این پست از این فیلد استفاده میکنیم تا در پروسه ثبت نام ایمیل‌ها را تصدیق کنیم. بدین منظور پس از ثبت نام کاربران یک ایمیل فعالسازی برای آنها ارسال می‌کنیم که حاوی یک لینک است. کاربران با کلیک کردن روی این لینک پروسه ثبت نام خود را تایید می‌کنند و می‌توانند به سایت وارد شوند. پیش از تایید پروسه ثبت نام، کاربران قادر به ورود نیستند.


در ابتدا باید اطلاعات کلاس کاربر را تغییر دهید تا دو فیلد جدید را در بر گیرد. یک فیلد شناسه تایید (confirmation token) را ذخیره می‌کند، و دیگری فیلدی منطقی است که مشخص می‌کند پروسه ثبت نام تایید شده است یا خیر. پس کلاس ApplicationUser  حالا باید بدین شکل باشد.
public class ApplicationUser : IdentityUser
{
    public string Email { get; set; }
    public string ConfirmationToken { get; set; }
    public bool IsConfirmed { get; set; }
}
اگر پیش از این کلاس ApplicationUser را تغییر داده اید، باید مهاجرت‌ها را فعال کنید و دیتابیس را بروز رسانی کنید. حالا می‌توانیم از این اطلاعات جدید در پروسه ثبت نام  استفاده کنیم و برای کاربران ایمیل‌های تاییدیه را بفرستیم.
private string CreateConfirmationToken()
{
    return ShortGuid.NewGuid();
}
 
private void SendEmailConfirmation(string to, string username, string confirmationToken)
{
    dynamic email = new Email("RegEmail");
    email.To = to;
    email.UserName = username;
    email.ConfirmationToken = confirmationToken;
    email.Send();
}
 
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        string confirmationToken = CreateConfirmationToken();
        var user = new ApplicationUser()
        {
            UserName = model.UserName,
            Email = model.Email,
            ConfirmationToken = confirmationToken, 
                IsConfirmed = false };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            SendEmailConfirmation(model.Email, model.UserName, confirmationToken);
            return RedirectToAction("RegisterStepTwo", "Account");
        }
        else
        {
            AddErrors(result);
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}
برای تولید شناسه‌های تایید (tokens) از کلاسی بنام ShortGuid استفاده شده است. این کلاس یک مقدار GUID را encode می‌کند که در نتیجه آن مقدار خروجی کوتاه‌تر بوده و برای استفاده در URL‌ها ایمن است. کد این کلاس را از این وبلاگ گرفته ام. پس از ایجاد حساب کاربری باید شناسه تولید شده را به آن اضافه کنیم و مقدار فیلد IsConfirmed را به false تنظیم کنیم. برای تولید ایمیل‌ها من از Postal استفاده می‌کنم. Postal برای ساختن ایمیل‌های دینامیک شما از موتور Razor استفاده می‌کند. می‌توانید ایمیل‌های ساده (plain text) یا HTML بسازید، عکس و فایل در آن درج و ضمیمه کنید و امکانات بسیار خوب دیگر. اکشن متد RegisterStepTwo تنها کاربر را به یک View هدایت می‌کند که پیامی به او نشان داده می‌شود.
بعد از اینکه کاربر ایمیل را دریافت کرد و روی لینک تایید کلیک کرد به اکشن متد RegisterConfirmation باز می‌گردیم.
private bool ConfirmAccount(string confirmationToken)
{
    ApplicationDbContext context = new ApplicationDbContext();
    ApplicationUser user =  context.Users.SingleOrDefault(u => u.ConfirmationToken == confirmationToken);
    if (user != null)
    {
        user.IsConfirmed = true;
        DbSet<ApplicationUser> dbSet = context.Set<ApplicationUser>();
        dbSet.Attach(user);
        context.Entry(user).State = EntityState.Modified;
        context.SaveChanges();
 
        return true;
    }
    return false;
}
 
[AllowAnonymous]
public ActionResult RegisterConfirmation(string Id)
{
    if (ConfirmAccount(Id))
    {
        return RedirectToAction("ConfirmationSuccess");
    }
    return RedirectToAction("ConfirmationFailure");
}
متد ConfirmAccount سعی می‌کند کاربری را در دیتابیس پیدا کند که شناسه تاییدش با مقدار دریافت شده از URL برابر است. اگر این کاربر پیدا شود، مقدار خاصیت IsConfirmed را به true تغییر می‌دهیم و همین مقدار را به تابع باز می‌گردانیم. در غیر اینصورت false بر می‌گردانیم. اگر کاربر تایید شده است، می‌تواند به سایت وارد شود. برای اینکه مطمئن شویم کاربران پیش از تایید ایمیل شان نمی‌توانند وارد سایت شوند، باید اکشن متد Login را کمی تغییر دهیم.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindAsync(model.UserName, model.Password);
        if (user != null && user.IsConfirmed)
        {
            await SignInAsync(user, model.RememberMe);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "Invalid username or password.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}
تنها کاری که می‌کنیم این است که به دنبال کاربری می‌گردیم که فیلد IsConfirmed آن true باشد. اگر مقدار این فیلد false باشد کاربر را به سایت وارد نمی‌کنیم و پیغام خطایی نمایش می‌دهیم.
همین. این تمام چیزی بود که برای اضافه کردن تصدیق ایمیل به اپلیکیشن خود نیاز دارید. از آنجا که سیستم ASP.NET Identity با Entity Framework مدیریت می‌شود و با مدل Code First ساخته شده، سفارشی کردن اطلاعات کاربران و سیستم عضویت ساده‌تر از همیشه است.



توضیحاتی درباره کار با Postal

اگر به متد SendEmailConfirmation دقت کنید خواهید دید که آبجکتی از نوع Email می‌سازیم (که در اسمبلی‌های Postal وجود دارد) و از آن برای ارسال ایمیل استفاده می‌کنیم. عبارت "RegEmail" نام نمایی است که باید برای ساخت ایمیل استفاده شود. این متغیر از نوع dynamic است، مانند خاصیت ViewBag. بدین معنا که می‌توانید مقادیر مورد نظر خود را بصورت خواص دینامیک روی این آبجکت تعریف کنید. از آنجا که Postal از موتور Razor استفاده می‌کند، بعدا در View ایمیل خود می‌توانید به این مقادیر دسترسی داشته باشید.
در پوشه Views پوشه جدیدی بنام Emails بسازید. سپس یک فایل جدید با نام RegEmail.cshtml در آن ایجاد کنید. کد این فایل را با لیست زیر جایگزین کنید.
To: @ViewBag.To
From: YOURNAME@gmail.com
Subject: Confirm your registration

Hello @ViewBag.UserName,
Please confirm your registration by following the link bellow.

@Html.ActionLink(Url.Action("RegisterConfirmation", "Account", new { id = @ViewBag.ConfirmationToken }), "RegisterConfirmation", "Account", new { id = @ViewBag.ConfirmationToken }, null)
این فایل، قالب ایمیل‌های شما خواهد بود. ایمیل‌ها در حال حاظر بصورت plain text ارسال می‌شوند. برای اطلاعات بیشتر درباره ایمیل‌های HTML و امکانات پیشرفته‌تر به سایت پروژه Postal  مراجعه کنید.

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

  • ViewBag.To آدرس ایمیل گیرنده را نشان می‌دهد.
  • ViewBag.UserName نام کاربر جاری را نمایش می‌دهد.
  • ViewBag.ConfirmationToken شناسه تولید شده برای تایید کاربر است.
در این قالب لینکی به متد RegisterConfirmation در کنترلر Account وجود دارد که شناسه تایید را نیز با پارامتری بنام id انتقال می‌دهد.

یک فایل ViewStart.cshtml_ هم در این پوشه بسازید و کد آن را با لیست زیر جایگزین کنید.
@{ Layout = null; /* Overrides the Layout set for regular page views. */ }
مطالب
نمایش، ذخیره و چاپ فایل‌های PDF در برنامه‌های Angular
با توجه به اینکه فایل‌های PDF نیز فایل باینری هستند، کلیات نکات مطلب «دریافت و نمایش تصاویر از سرور در برنامه‌های Angular» در مورد آن‌ها هم صادق است. در اینجا به تکمیل این نکات پرداخته و مواردی را مانند ذخیره، چاپ و استفاده از اشیاء نمایشی <object>، <embed> و <iframe> نیز بررسی می‌کنیم. نمایش PDF در اینجا بر اساس امکانات توکار مرورگرها صورت می‌گیرد و نیاز به افزونه‌ی اضافه‌تری ندارد.


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

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

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class ReportsController : Controller
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            return File(virtualPath: "~/assets/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
که در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.


سرویس دریافت محتوای باینری در برنامه‌های Angular

برای اینکه HttpClient برنامه‌های Angular بتواند محتوای باینری را بجای محتوای JSON پیش‌فرض آن دریافت کند، نیاز است نوع خروجی سمت سرور آن‌را به blob تنظیم کرد:
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { HttpClient } from "@angular/common/http";

@Injectable()
export class DownloadPdfDataService {

  constructor(private httpClient: HttpClient) { }

  public getReport(): Observable<Blob> {
    return this.httpClient.get("/api/Reports/GetPdfReport", { responseType: "blob" });
  }
}
به این ترتیب پس از اشتراک به متد getReport این سرویس، اطلاعات باینری این فایل PDF را دریافت خواهیم کرد.


اصلاح Content Security Policy سمت سرور جهت ارائه‌ی محتوای blob

پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob:
default-src 'self' blob:
object-src 'self' blob:
در غیراینصورت مرورگر نمایش یک چنین تصاویر و یا اشیایی را سد خواهد کرد.


دریافت فایل‌های PDF از سرور و نمایش آن‌ها در یک برنامه‌ی Angular

پس از این مقدمات، کامپوننتی که یک فایل PDF را از سمت سرور دریافت کرده و نمایش می‌دهد، چنین کدی را خواهد داشت:
import { DownloadPdfDataService } from "./../download-pdf-data.service";
import { WindowRefService } from "./../../core/window.service";
import { Component, OnInit } from "@angular/core";
import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser";

@Component({
  templateUrl: "./view-pdf.component.html",
  styleUrls: ["./view-pdf.component.css"]
})
export class ViewPdfComponent implements OnInit {
  private nativeWindow: Window;
  private pdfBlobUrl: string;
  sanitizedPdfBlobResourceUrl: SafeResourceUrl;

  constructor(private downloadService: DownloadPdfDataService,
    private windowRefService: WindowRefService, private sanitizer: DomSanitizer) { }

  ngOnInit() {
    this.nativeWindow = this.windowRefService.nativeWindow;
    this.downloadService.getReport().subscribe(pdfDataBlob => {
      console.log("pdfDataBlob", pdfDataBlob);
      const urlCreator = this.nativeWindow.URL;
      this.pdfBlobUrl = urlCreator.createObjectURL(pdfDataBlob);
      console.log("pdfBlobUrl", this.pdfBlobUrl);
      this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl);
    });
  }
}
با این قالب:
<h1>Display PDF Files</h1>

<div *ngIf="sanitizedPdfBlobResourceUrl">
  <h4>using iframe</h4>
  <iframe width="100%" height="600" [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf"></iframe>
  <h4>using object</h4>
  <object [attr.data]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%"
    height="100%"></object>
  <h4>usin embed</h4>
  <embed [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%"
    height="100%">
</div>
- در اینجا در ngOnInit، به سرویس پنجره دسترسی یافته و وهله‌ای از آن‌را جهت کار با متد createObjectURL شیء URL آن دریافت می‌کنیم.
- سپس مشترک متد getReport دریافت فایل PDF شده و اطلاعات نهایی آن‌را به صورت pdfDataBlob دریافت می‌کنیم.
- این اطلاعات باینری را به متد createObjectURL ارسال کرده و آدرس موقتی این تصویر را در مرورگر بدست می‌آوریم.
- چون در این حالت Angular این URL را امن سازی می‌کند، یک چنین خروجی unsafe:blob بجای blob تولید خواهد شد که نمایش این مورد نیز توسط مرورگر سد می‌شود. برای رفع این مشکل، می‌توان از سرویس DomSanitizer آن که به سازنده‌ی کلاس تزریق شده‌است استفاده کرد:
this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl);
تفاوت این مورد با حالت نمایش تصویر، استفاده از متد bypassSecurityTrustResourceUrl بجای متد bypassSecurityTrustUrl است. از این جهت که اشیاء یاد شده نیاز به SafeResourceUrl دارند و نه SafeUrl.
اینبار یک چنین انتسابی به صورت مستقیم کار می‌کند که سه نمونه‌ی این انتساب را به اشیاء iframe ،object و embed، در قالب فوق مشاهده می‌کنید.



افزودن دکمه‌ی چاپ PDF به برنامه

پس از اینکه به this.pdfBlobUrl دسترسی یافتیم، اکنون می‌توان یک iframe مخفی را ایجاد کرد، سپس src آن‌را به این آدرس ویژه تنظیم نمود و در آخر متد print آن‌را فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر می‌شود:
  printPdf() {
    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = this.pdfBlobUrl;
    document.body.appendChild(iframe);
    iframe.contentWindow.print();
  }


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

اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگه‌ای جدید نمایش دهید، می‌توان از متد window.open استفاده کرد:
  showPdf() {
    this.nativeWindow.open(this.pdfBlobUrl);
  }


دریافت فایل PDF

بجای نمایش فایل PDF می‌توان دکمه‌ای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
  downloadPdf() {
    const fileName = "test.pdf";
    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = this.pdfBlobUrl;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
  }
در اینجا یک anchor جدید به صورت مخفی به صفحه اضافه می‌شود که href آن به this.pdfBlobUrl تنظیم شده‌است. سپس متد click آن فراخوانی خواهد شد. نام این فایل را هم توسط ویژگی download این شیء می‌توان تنظیم نمود.
این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایل‌های PDF نیست.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
ساخت یک Form Generator ساده در MVC
- برای دریافت اطلاعات یک فرم مشخص:
        public IList<Value> GetValues(int formId)
        {
            return _values.Include(x => x.Field)
                          .Where(value => value.FormId == formId)
                          .ToList();
        }
- این پروژه از دیدگاه دریافت اطلاعات از کاربر و همچنین تولید فرم پویا جالب است (صرفا قسمت UI آن). به لطف نبود ViewState، طراحی فرم‌های پویا در اینجا خیلی ساده‌تر است از وب‌فرم‌ها.
- اما از دیدگاه ذخیره سازی این اطلاعات پویا ... مشکل دارد. در طراحی بانک اطلاعاتی آن فرض شده‌است که برنامه مثلا فرم یک را دارد. این فرم یک، 10 فیلد پویا یا بیشتر را دارد. این 10 فیلد فقط یکبار توسط کاربر پر می‌شوند. اگر کاربر قرار باشد بار دوم این فرم یک را پر کند، امکان پذیر نیست. در کلاس Value آن که فقط محتوای یک فیلد را ذخیره می‌کند، مفهومی به نام ردیف سند جاری وجود ندارد. به ازای هر فیلد یک ردیف مجزا داریم؛ اما مشخص نیست این ردیف‌ها متعلق به کدام سند هستند (منظور از سند، شمار منحصربفرد پر کردن فرم جاری است؛ هر کاربر یک فرم را بیش از یکبار می‌تواند پر کند).
 برای حل این نوع مشکلات (برای اینکه به ازای مقدار هر فیلد فرم پویا، یک ردیف مجزا تولید نشود و schemaless کار کرد) یا باید از یک بانک اطلاعاتی NoSQL استفاده کرد که مثلا کل فرم را در قالب یک شیء JSON سریالایز کند و آن‌را داخل یک فیلد از یک ردیف (یک سند) ذخیره کند. یا اگر با SQL Server کار می‌کنید، فیلد XML آن چنین قابلیتی را دارد. کل اطلاعات دریافتی فرم را تبدیل به XML کنید و سپس ذخیره. به این صورت امکان تهیه کوئری و گزارش گرفتن‌های پیشرفته و بهینه را به همراه تعریف ایندکس و مسایل دیگر نیز خواهید داشت. اینبار هر ردیف بانک اطلاعاتی، مفهوم یک سند کامل را پیدا می‌کند؛ بجای اینکه هر ردیف فقط یک مقدار از یک فیلد باشد. همچنین در این حالت هر ردیف می‌تواند محتوای فرمی را ذخیره کند که با ردیف بعدی کاملا متفاوت است (بر اساس طراحی پویای متفاوت هر فرم).
مطالب
جلوگیری از ورود نام Area های یکسان، در هنگام درج اطلاعات در برنامه‌های ASP.NET MVC 5x
در وب‌سایتی مثل آپارات، چنین آدرسی aparat.com/reporting به منزله‌ی آدرس دهی به کانال شخصیِ فردی است. حال اگر وب‌سایت ما نیز چنین سیستم آدرس دهی را داشته باشد و همچنین پیشتر یک Area با نام Reporting را نیز داشته باشیم، توسط چنین آدرسی (درحالت پیش فرض) به آن Area دسترسی خواهیم داشت:
mysite.com/reporting
حال اگر یکی از کاربران هنگام ساخت کانالی جدید (برای سناریوی بالا)، بخواهد آدرس کانالش Reporting باشد، با توجه به اینکه هم مسیر دسترسی به Area گزارشات (Reporting) و هم مسیر دسترسی به کانال این شخص از طریق Url بالا است، قطعا به مشکل خواهیم خورد.
برای رفع این مشکل میتوان یک فایل xml، txt و ... درست کرد و نام تمامی Area‌‌ها را در آن فایل ثبت کرد و بعد، هنگام ثبت کانال جدید (برای سناریوی بالا) توسط کاربر، فایل مذکور را خوانده و در صورتیکه نام آدرس وارد شده معادل یکی از Area‌‌های سایتمان بود و در لیست Area‌‌های از پیش ثبت شده در آن فایل قرار داشت، پیغام لازم را به کاربر نشان می‌دهیم و از ثبت و یا ویرایش اطلاعات، جلوگیری می‌کنیم.
روش فوق به درستی کار می‌کند و مشکلی ندارد، اما ضعف آن این است که به صورت دستی این عملیات باید انجام شود و در صورتیکه یک Area جدید اضافه شود، باید آن فایل ویرایش شود. اما می‌توان با استفاده از یک Attribute، این کار را انجام و تمامی عملیات را به صورت داینامیک انجام داد.
برای شروع، یک مدل برای کانال و یک منبع داده را برای آن در نظر می‌گیریم:
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        public string ChannelUrl { get; set; }
    }
}
using System.Collections.Generic;

namespace SampleProject.Models
{
    public static class ChannelDataSource
    {
        static ChannelDataSource() => Channels = new List<Channel>();
        public static List<Channel> Channels { get; private set; }
        public static void Add(Channel channel) => Channels.Add(channel);
    }
}
منبع داده، شامل یک خاصیت است که لیست تمامی کانال‌های از قبل اضافه شده را بر می‌گرداند و یک متد افزودن که به این لیست، یک کانال را اضافه می‌کند.
حال یک کنترلر به نام Channel را اضافه می‌کنیم:
using SampleProject.Models;
using System.Linq;
using System.Web.Mvc;

namespace SampleProject.Controllers
{
    public class ChannelController : Controller
    {
        // GET: Channel
        public ActionResult Index()
        {
            var channels = ChannelDataSource.Channels;
            return View(channels);
        }

        public ActionResult Channel(string channelUrl)
        {
            if (string.IsNullOrWhiteSpace(channelUrl))
            {
                return new HttpNotFoundResult("channel not found!");
            }
            var channel = ChannelDataSource.Channels.SingleOrDefault(ch => ch.ChannelUrl == channelUrl.ToLower());
            if (channel == null)
            {
                return new HttpNotFoundResult("channel not found!");
            }
            return View(channel);
        }

        public ActionResult Create() => View();

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Channel channel)
        {
            if (!ModelState.IsValid)
            {
                ModelState.AddModelError(string.Empty, "Please check your inputs!");
                return View(channel);
            }
            ChannelDataSource.Add(channel);
            TempData["Message"] = "Channel added successfully!";
            return RedirectToAction(nameof(Index));
        }
    }
}
در اکشن Index، لیستی از تمامی کانال‌های موجود را نمایش می‌دهیم. در اکشن Channel، آدرسی را که وارد شده است، در منبع داده به دنبال آن می‌گردیم و یک ویوو با Template جزئیات (Details)، از مدل کانال را به کاربر نمایش می‌دهیم؛ در غیر اینصورت صفحه 404 را نمایش می‌دهیم. در اکشن‌های Create، صفحه افزودن را به کاربر نمایش داده و در آن یکی اکشن، عمل افزودن را در صورتیکه اطلاعات وارد شده صحیح باشند، انجام می‌دهیم.
با توجه به اینکه میخواهیم سیستم مسیر دهی سایت برای کانال‌ها تغییر کند، فایل RouteConfig در پوشه‌ی App_Start را به شکل ذیل تغییر می‌دهیم:
using System.Web.Mvc;
using System.Web.Routing;

namespace SampleProject
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "ChannelUrls",
                url: "{channelurl}",
                defaults: new { controller = "Channel", action = "Channel", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Channel", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در مسیر دهی بالا اگر "نام سایت، اسلش، نام کانال" را وارد کند اولین سیستم مسیریابی فعال می‌شود و او را به اکشن Channel کنترلر Channel، راهنمایی می‌کند.
حال برای اینکه هنگام ساخت کانال جدید، نام تکراری یکی از Area‌ها را وارد نکند، به این ترتیب عمل می‌کنیم:
ابتدا یک متد کمکی را نوشته که لیست Area‌‌های پروژه‌مان را برگشت دهد ( + ):
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;

namespace SampleProject.Models
{
    public static class Utility
    {
        public static List<string> GetAllAreaNames()
        {
            var areaNames = RouteTable.Routes.OfType<Route>()
                            .Where(d => d.DataTokens != null)
                            .Where(d=> d.DataTokens.ContainsKey("area"))
                            .Select(r => r.DataTokens["area"].ToString().ToLower())
                            .ToList();
            return areaNames;
        }
    }
}
و بعد Attribute مورد نظر را ایجاد میکنیم:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using SampleProject.Models;

namespace SampleProject.CustomValidators
{
    public class CheckForAreaExisting : ValidationAttribute, IClientValidatable
    {
        public List<string> AreaNames
        {
            get
            {
                return Utility.GetAllAreaNames();
            }
        }
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ValidationType = "checkforareaexisting",
                ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
            };
            rule.ValidationParameters.Add("areanames", string.Join(",", AreaNames));
            yield return rule;
        }

        public override bool IsValid(object value)
        {
            if (value != null)
            {
                return Utility.GetAllAreaNames()
                   .SingleOrDefault(area => area == value.ToString().ToLower()) == null;
            }
            return true;
        }
    }
}
در کلاس بالا توسط متد IsValid بررسی میکنیم که آیا مقدار وارد شده ( Channel Url ) با یکی از نام‌های Area‌‌های پروژه‌مان تطابق دارد یا خیر، که اگر این چنین بود، مقدار false برگشت داده می‌شود.
توسط واسط IClientValidatable و متود GetClientValidationRules کارهای اعتبارسنجی سمت کلاینت را نیز انجام می‌دهیم ( + ). مقدار خاصیت ValidationType نام متدی است که در سمت کلاینت این کار را انجام می‌دهد. مقدار خاصیت ValidationParameters، مقداری است که به سمت کلاینت به عنوان param فرستاده می‌شود تا از آن جهت اینکه آیا مقدار وارد شده توسط کاربر، یکی از Area‌های سایت هست یا خیر، استفاده کرد. در اینجا نام Area‌‌‌ها را با یک رشته و با یک جداکننده، توسط این خاصیت به سمت کلاینت می‌فرستیم. 
حال در سمت کلاینت یک فایل Js را با نام CustomValidation و محتوای زیر ایجاد می‌کنیم:
jQuery.validator.addMethod("checkforareaexisting",
    function (value, element, param) {
        var isValueOneOfTheAreaNames = $.inArray(value.toLowerCase(), param.areaNames) === -1;
        return isValueOneOfTheAreaNames;
    });

$.validator.unobtrusive.adapters.add('checkforareaexisting', ['areanames'], function (options) {
    options.rules['checkforareaexisting'] = { areaNames: options.params.areanames.split(',') };
    options.messages['checkforareaexisting'] = options.message;
});
در بخش اول، نام متد که در بالا (Attribute) به آن اشاره شده است آمده است، و بعد بررسی می‌کنیم که آیا مقدار آمده توسط کاربر، یکی از نام‌های Area‌‌های موجود سایت است یا خیر که اگر این طور باشد، false برگشت داده می‌شود و پیغام خطا به کاربر نمایش داده می‌شود. در بخش Onubtrusive توسط پارامتری که در Attribute برای فرستادن نام Area‌ها نوشته بودیم (areanames)، نام‌های Area‌ها را می‌گیریم و بعد آن را Split و به Rule انتساب می‌دهیم و ErrorMessage ـی را که به خاصیت ChannelUrl مدلمان نسبت می‌دهیم، به عنوان پیغام خطا در نظر می‌گیریم.
فایل‌های Js در Layout باید به این صورت باشند:
<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>_Layout</title>
    <style>

    </style>
</head>
<body>
    <div>
        @RenderBody()
    </div>
    <script src="~/Scripts/Jquery.js"></script>
    <script src="~/Scripts/jquery.validate.min.js"></script>
    <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
    <script src="~/Scripts/CustomValidation.js"></script>
</body>
</html>

حال کافی است به خاصیت ChannelUrl مدلمان این Attribute را نسبت دهیم:
using SampleProject.CustomValidators;
using System.ComponentModel.DataAnnotations;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [CheckForAreaExisting(ErrorMessage = "You can't use this url for your channel!")]
        public string ChannelUrl { get; set; }
    }
}
اکنون نوبت آزمایش برنامه است. کافی است که یک یا چند Area جدید را با نام‌های متفاوت، اضافه کنید و الان اگر به صفحه افزودن کانال مراجعه کنید و نام یکی از Area‌‌های سایت را در قسمت Channel Url وارد کنید، پیغام خطا نمایش داده می‌شود.
نکته: در این حالت اسامی تمامی Area‌‌های سایت به کلاینت ارسال می‌شود. اگر از این بابت احساس رضایت نمی‌کنید، میتوانید از خاصیت Remote توکار MVC بهره ببرید.
برای اینکار این اکشن را به کنترلر Channel اضافه می‌کنیم:
[HttpPost]
public ActionResult CheckForAreaExisting(string channelUrl)
{
    var isValueOneOfTheAreaNames = Utility.GetAllAreaNames()
                                   .SingleOrDefault(area => area == channelUrl.ToLower()) == null;
    return Json(isValueOneOfTheAreaNames);
}  
و بعد مدل نیز به این صورت تغییر می‌کند:
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace SampleProject.Models
{
    public class Channel
    {
        public string ChannelTitle { get; set; }
        [Required]
        [Remote("CheckForAreaExisting", "Channel",
            ErrorMessage = "You can't use this url for your channel!",
            HttpMethod = "Post")]
        public string ChannelUrl { get; set; }
    }
}
به این ترتیب هر بار درخواستی به سمت سرور ارسال و طی آن بررسی می‌شود که مقدار وارد شده یکی از Area‌‌‌‌های سایت هست یا خیر؟ بدیهی است که در این حالت، دیگر نیازی به واسط IClientValidatable در کلاس CheckForAreaExisting موجود در پوشه CustomValidators وجود ندارد.
مطالب
بهبود SEO در ASP.NET MVC
گوگل خلاصه نتایج Indexing یک سایت را توسط ابزاری به نام Google webmaster tools در اختیار علاقمندان قرار می‌دهد. Bing نیز چنین ابزاری را تدارک دیده است.
به آمارهای خطای حاصل از سایت جاری که دقت می‌کردم یک نکته آن جالب بود: «محتوای تکراری»


mydomain.com/Home/Index
mydomain.com/home/index
mydomain.com/Home/index
mydomain.com/home/Index
همانطور که ملاحظه می‌کنید، گوگل به کوچکی و بزرگی حروف بکار رفته در لینک‌ها حساس است. هرچند 4 لینک فوق به یک صفحه اشاره می‌کنند، اما گوگل 4 بار آن‌ها را ایندکس خواهد کرد و نهایتا به صورت یک خطای «محتوای تکراری» در گزارشات SEO آن ظاهر خواهد شد (به همراه کاهش رتبه SEO سایت).

راه حل

برای حل این مساله دو نکته باید درنظر گرفته شود:
الف) هدایت دائمی (Redirect permanent) صفحات قدیمی به صفحاتی جدید، با آدرس lowercase

using System.Globalization;
using System.Web;
using System.Web.Mvc;

namespace WebToolkit
{
    public class ForceWww : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            modifyUrlAndRedirectPermanent(filterContext);
            base.OnActionExecuting(filterContext);
        }

        private static void modifyUrlAndRedirectPermanent(ActionExecutingContext filterContext)
        {
            if (canIgnoreRequest(filterContext))
                return;

            var absoluteUrl = HttpUtility.UrlDecode(filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture));
            var absoluteUrlToLower = absoluteUrl.ToLowerInvariant();

            absoluteUrlToLower = forceWwwAndLowercase(filterContext, absoluteUrlToLower);
            absoluteUrlToLower = avoidTrailingSlashes(filterContext, absoluteUrlToLower);

            if (!absoluteUrl.Equals(absoluteUrlToLower))
            {
                filterContext.Result = new RedirectResult(absoluteUrlToLower, permanent: true);
            }
        }

        private static string avoidTrailingSlashes(ActionExecutingContext filterContext, string absoluteUrlToLower)
        {
            if (!isRootRequest(filterContext) && absoluteUrlToLower.EndsWith("/"))
                return absoluteUrlToLower.TrimEnd(new[] { '/' });

            return absoluteUrlToLower;
        }

        private static bool isRootRequest(ActionExecutingContext filterContext)
        {
            return filterContext.RequestContext.HttpContext.Request.Url.AbsolutePath == "/";
        }

        private static bool canIgnoreRequest(ActionExecutingContext filterContext)
        {
            return filterContext.IsChildAction || 
                   filterContext.HttpContext.Request.IsAjaxRequest() ||
                   filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.Contains("?");
        }

        private static string forceWwwAndLowercase(ActionExecutingContext filterContext, string absoluteUrlToLower)
        {
            if (isLocalRequet(filterContext))
                return absoluteUrlToLower;

            if (absoluteUrlToLower.Contains("www"))
                return absoluteUrlToLower;

            return absoluteUrlToLower.Replace("http://", "http://www.")
                                     .Replace("https://", "https://www.");
        }

        private static bool isLocalRequet(ActionExecutingContext filterContext)
        {
            return filterContext.RequestContext.HttpContext.Request.IsLocal;
        }
    }
}
کلاس فوق، نگارش تکمیل شده ForceWww که پیشتر در این سایت دیده‌اید. توسط آن سه بررسی مختلف بر روی لینک جاری در حال پردازش صورت خواهد گرفت:
- تمام آدرس‌های سایت باید www داشته باشند؛ تا آدرس‌های آن یکنواخت شده و خصوصا مشکلات لاگین و نوشته شدن کوکی‌ها به ازای آدرس‌های مختلف و سر درگمی کاربران کاهش یابد.
- اگر آدرس جاری lowercase نباشد، تبدیل به نمونه lowercase شده و درخواست کننده، به آدرس جدید هدایت می‌شود. این مورد خصوصا جهت موتورهای جستجو برای تصحیح نتایج آن‌ها بسیار مفید است.
- اسلش انتهای لینک‌ها در صورت وجود حذف خواهد شد. این مورد نیز در کاهش تعداد خطاهای «محتوای تکراری» مؤثر است.
- اگر آدرسی، کوئری استرینگ داشته باشد از آن صرفنظر خواهد شد؛ زیرا ممکن است اطلاعات موجود در آن به کوچکی و بزرگی حروف حساس باشند.


ب) کاهش بار سایت توسط تولید خودکار Urlهایی که در بدو امر lowercase هستند

برای پیاده سازی این مطلب می‌توان از پروژه سورس باز «LowercaseRoutesMVC» استفاده کرد. سه فایل cs دارد که می‌توانید به پروژه خود اضافه کنید. پس از آن، هرجایی در پروژه خود routes.MapRoute دارید تبدیل کنید به routes.MapRouteLowercase .
به این ترتیب به صورت خودکار تمام Urlهای تولید شده توسط HTML helpers توکار ASP.NET MVC (و نه Urlهایی که دستی نوشته شده‌اند)، در حین درج در صفحه به صورت lowercase ظاهر خواهند شد (صرفنظر از اینکه نام‌های کنترلرها و یا اکشن متدهای تعریف شده camel case هستند یا خیر). مزیت این مساله کاهش یک مرحله Redirect است که در قسمت الف ذکر شد. در این کتابخانه کمکی نیز از آدرس‌هایی که دارای کوئری استرینگ باشند، صرفنظر می‌شود.
مطالب
نمایش بلادرنگ اعلامی به تمام کاربران در هنگام درج یک رکورد جدید
در ادامه می‌خواهیم اعلام عمومی نمایش افزوده شدن یک پیام جدید را بعد از ثبت رکوردی جدید، به تمامی کاربران متصل به سیستم ارسال کنیم. پیش نیاز مطلب جاری موارد زیر می‌باشند:
namespace ShowAlertSignalR.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public float Price { get; set; }
        public Category Category { get; set; }

    }

    public enum Category
    {
        [Display(Name = "دسته بندی اول")]
        Cat1,
        [Display(Name = "دسته بندی دوم")]
        Cat2,
        [Display(Name = "دسته بندی سوم")]
        Cat3
    }
}
در اینجا مدل ما شامل عنوان، توضیح، قیمت و یک enum برای دسته‌بندی یک محصول ساده می‌باشد.
کلاس context نیز به صورت زیر می‌باشد:
namespace ShowAlertSignalR.Models
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext() : base("productSample")
        {
            Database.Log = sql => Debug.Write(sql);
        }
        public DbSet<Product> Products { get; set; }
    }
}
همانطور که در ابتدا عنوان شد، می‌خواهیم بعد از ثبت یک رکورد جدید، پیامی عمومی به تمامی کاربران متصل به سایت نمایش داده شود. در کد زیر اکشن متد Create را مشاهده می‌کنید: 
[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Product product)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(product);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            return View(product);
        }
می‌توانیم از ViewBag برای اینکار استفاده کنیم؛ به طوریکه یک پارامتر از نوع bool برای متد Index تعریف کرده و سپس مقدار آن را درون این شیء ViewBag انتقال دهیم، این متغییر بیانگر حالتی است که آیا اطلاعات جدیدی برای نمایش وجود دارد یا خیر؟ بنابراین اکشن متد Index را به اینصورت تعریف می‌کنیم:
public ActionResult Index(bool notifyUsers = false)
        {
            ViewBag.NotifyUsers = notifyUsers;
            return View(db.Products.ToList());
        }
در اینجا مقدار پیش‌فرض این متغیر، false می‌باشد. یعنی اطلاعات جدیدی برای نمایش موجود نمی‌باشد. در نتیجه اکشن متد Create را به صورتی تغییر می‌دهیم که بعد از درج رکورد موردنظر و هدایت کاربر به صفحه‌ی Index، مقدار این متغییر به true تنظیم شود:
[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Product product)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(product);
                db.SaveChanges();
                return RedirectToAction("Index", new { notifyUsers = true });
            }

            return View(product);
        }
قدم بعدی ایجاد یک هاب SignalR می‌باشد:
namespace ShowAlertSignalR.Hubs
{
    public class NotificationHub : Hub
    {
        public void SendNotification()
        {
            Clients.Others.ShowNotification();
        }
    }
}
در ادامه کدهای سمت کلاینت را برای هاب فوق، داخل ویوی Index اضافه می‌کنیم:
@section scripts
{
    
    <script src="~/Scripts/jquery.signalR-2.0.2.min.js"></script>
    <script src="~/signalr/hubs"></script>
    <script>

        var notify = $.connection.notificationHub;
        notify.client.showNotification = function() {
            $('#result').append("<div class='alert alert-info alert-dismissable'>" +
                "<button type='button' class='close' data-dismiss='alert' aria-hidden='true'>&times;</button>" +
            "رکورد جدیدی هم اکنون ثبت گردید، برای مشاهده آن صفحه را بروزرسانی کنید" + "</div>");
        };
        $.connection.hub.start().done(function() {
            @{
                if (ViewBag.NotifyUsers)
                {
                    <text>notify.server.sendNotification();</text>
                }
            }
        });
    </script>
}
همانطور که در کدهای فوق مشاهده می‌کنید، بعد از اینکه اتصال با موفقیت برقرار شد (درون متد done) شرط چک کردن متغییر NotifyUsers را بررسی کرده‌ایم. یعنی در این حالت اگر مقدار آن true بود، متد درون هاب را فراخوانی کرده‌ایم. در نهایت پیام به یک div با آی‌دی result اضافه شده است.
لازم به ذکر است برای حالت‌های حذف و به‌روزرسانی نیز روال کار به همین صورت می‌باشد.
سورس مثال جاری : ShowAlertSignalR.zip