مطالب
ASP.NET MVC #6

آشنایی با انواع ActionResult

در قسمت چهارم، اولین متد یا اکشنی که به صورت خودکار توسط VS.NET به برنامه اضافه شد، اینچنین بود:

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}

توضیحات تکمیلی مرتبط با خروجی از نوع ActionResult ایی را که مشاهده می‌کنید، در این قسمت ارائه خواهد شد.
رفتار یک کنترلر توسط متدهایی که در آن کلاس تعریف می‌شوند، مشخص می‌گردد. هر متد هم از طریق یک URL مجزا قابل دسترسی و فراخوانی خواهد بود. این متدها که به آن‌ها اکشن نیز گفته می‌شود باید عمومی بوده، استاتیک یا متد الحاقی (extension method) نباشند و همچنین دارای پارامترهایی از نوع ref و out نیز نباشند.
هر درخواست رسیده، به یک کنترلر و متدی عمومی در آن توسط سیستم مسیریابی، نگاشت خواهد شد. اگر علاقمند باشید که در یک کلاس کنترلر، متدی عمومی را از این سیستم خارج کنید، تنها کافی است آن‌را با ویژگی (attribute) به نام NonAction مزین کنید:

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
public class HomeController : Controller
{
[NonAction]
public string ShowData()
{
return "Text";
}

public ActionResult Index()
{
ViewBag.Message = string.Format("{0}/{1}/{2}",
RouteData.Values["controller"],
RouteData.Values["action"],
RouteData.Values["id"]);
return View();
}

public ActionResult Search(string data = "*")
{
// do something ...
return View();
}
}
}

چند نکته در این مثال قابل ذکر است:
الف) در اینجا اگر شخصی آدرس http://localhost/home/showdata را درخواست نماید، با توجه به استفاده از ویژگی NonAction، با پیغام یافت نشد یا 404 مواجه می‌گردد.
ب) صرفنظر از پارامترهای یک متد و ساختار کلاس جاری، اطلاعات مسیریابی از طریق شیء RouteData.Values نیز در دسترس هستند که نمونه‌ای از آن‌را در اینجا بر اساس مقادیر پیش فرض تعاریف مسیریابی یک پروژه ASP.NET MVC ملاحظه می‌نمائید.
ج) در متد Search، از قابلیت امکان تعریف مقداری پیش فرض جهت آرگومان‌ها در سی شارپ 4 استفاده شده است. به این ترتیب اگر شخصی آدرس http://localhost/home/search را وارد کند، چون پارامتری را ذکر نکرده است، به صورت خودکار از مقدار پیش فرض آرگومان data استفاده می‌گردد.


انواع Action Results در ASP.NET MVC

در ASP.NET MVC بجای استفاده مستقیم از شیء Response، از شیء ActionResult جهت ارائه خروجی یک متد استفاده می‌شود و مهم‌ترین دلیل آن هم مشکل بودن نوشتن آزمون‌های واحد برای شیء Response است که وهله سازی آن مساوی است با به کار اندازی موتور ASP.NET و Http Runtime آن توسط یک وب سرور (بنابراین در ASP.NET MVC سعی کنید شیء Response را فراموش کنید).
سلسه مراتب ActionResult‌های قابل استفاده در ASP.NET در تصویر زیر مشخص شده‌اند:


و در مثال زیر تقریبا انواع و اقسام ActionResult‌های مهم و کاربردی ASP.NET MVC را می‌توانید مشاهده کنید:

using System.Web.Mvc;

namespace MvcApplication2.Controllers
{
public class ActionResultsController : Controller
{
//http://localhost/actionresults/welcome
public string Welcome()
{
return "Hello, World";
}

//http://localhost/actionresults/index
public ActionResult Index() // or ContentResult
{
return Content("Hello, World");
}

//http://localhost/actionresults/SendMail
public void SendMail()
{
}

public ActionResult SendMailCompleted() // or EmptyResult
{
// do whatever
return new EmptyResult();
}

public ActionResult GetFile() // or FilePathResult
{
return File(Server.MapPath("~/content/site.css"), "text/css", "mySite.css");
}

public ActionResult UnauthorizedStatus() // or HttpStatusCodeResult/HttpUnauthorizedResult
{
return new HttpUnauthorizedResult("You need to login first.");
}

public ActionResult Status() // or HttpStatusCodeResult
{
return new HttpStatusCodeResult(501, "Server Error");
}

public ActionResult GetJavaScript() // or JavaScriptResult
{
return JavaScript("...JavaScript...");
}

public ActionResult GetJson() // or JsonResult
{
var obj = new { prop1 = 1, prop2 = "data" };
return Json(obj, JsonRequestBehavior.AllowGet);
}

public ActionResult RedirectTo() // or RedirectResult
{
return RedirectPermanent("http://www.site.com");
//return RedirectToAction("Home", "Index");
}

public ActionResult ShowView() // or ViewResult
{
return View();
}
}
}

چند نکته در این مثال وجود دارد:
1) مثلا متد GetJavaScript را درنظر بگیرید. در این متد خاص، چه بنویسید public ActionResult GetJavaScript یا بنویسید public JavaScriptResult GetJavaScript تفاوتی نمی‌کند. در سایر موارد هم به همین ترتیب است. علت را در تصویر سلسله مراتبی ActionResult‌ها می‌توان جستجو کرد. تمام این کلاس‌ها نوعی ActionResult هستند و از یک کلاس پایه به ارث رسیده‌اند.
2) مثلا ContentResult شبیه به همان Response.Write سابق ASP.NET عمل می‌کند. علت وجودی آن هم عدم وابستگی مستقیم به شیء Response و ساده‌تر سازی نوشتن آزمون‌های واحد برای این نوع اکشن متدها است.
3) منهای متد آخری که نمایش داده شده (ShowView)، هیچکدام از متدهای دیگر نیازی به View متناظر ندارند. یعنی نیازی نیست تا روی متد کلیک راست کرده و Add view را انتخاب کنیم. چون در همین متد کنترلر، کار Response به پایان می‌رسد و مرحله بعدی ندارد. مثلا در حالت return File، یک فایل به درون مرورگر کاربر Flush خواهد شد و تمام.
4) متد Welcome و متد Index در اینجا به یک صورت تفسیر می‌شوند. به این معنا که اگر خروجی متد تعریف شده در یک کنترلر از نوع ActionResult نباشد، به صورت پیش فرض درون یک ContentResult محصور خواهد شد.
5) اگر خروجی متدی در اینجا از نوع void باشد، با ActionResult ایی به نام EmptyResult یکسان خواهد بود. بنابراین با متدهای SendMail و SendMailCompleted به یک نحو رفتار می‌گردد.
6) return Json یاد شده که خروجی‌اش از نوع JsonResultاست در پیاده سازی‌های Ajax ایی کاربرد دارد.
7) جهت بازگرداندن حالت وضعیت 403 یا غیرمجاز می‌توان از return new HttpUnauthorizedResult استفاده کرد.
8) یا جهت اعلام مشکلی در سمت سرور به کمک return new HttpStatusCodeResultکد ویژه‌ای را می‌توان به کاربر نمایش داد.
9) به کمک return RedirectToAction می‌توان به یک کنترلر و متدی خاص در آن، کاربر را هدایت کرد.

و خلاصه اینکه تمام کارهایی را که پیشتر در ASP.NET Web forms ، مستقیما به کمک شیء Response انجام می‌دادید (Response.Write، Response.End، Response.Redirect و غیره)، اینبار به کمک یکی از ActionResult‌های یاد شده انجام دهید تا بتوان بدون نیاز به راه اندازی یک وب سرور، برای متدهای کنترلرها آزمون واحد نوشت. برای مثال:

[TestMethod]
public void TestMethod1()
{
    // Arrange
    var controller = new ActionResultsController();

    // Act
    var result = controller.Index() as ContentResult;

    // Assert
    Assert.NotNull(result);
    Assert.AreEqual( "Hello, World", result.Content);
}



نظرات مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت پنجم - اعتبارسنجی تنظیمات آغازین برنامه
در یک پروژه nullable ref types فعال است و در دیگری خیر:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <Nullable>enable</Nullable>
  </PropertyGroup>
اگر می‌خواهید هر دو مثل هم عمل کنند، باید به اخطارهای کامپایلر دقت کنید و Name را nullable تعریف کنید:
public class Person
{
     public string? Name { get; set; }
     public string? Family { get; set; }
نظرات مطالب
قیود مسیریابی در ASP.NET Core
استفاده از URL Slug در آدرس‌دهی می‌تواند کمک کننده باشد؛ به این صورت که در زمان تولید لینک، مقدار مناسب را از روی مقدار اصلی با این شیوه تولید و جایگزین کنید. در بعضی از پیاده‌سازی‌ها برای این منظور در پایگاه داده فیلدی مجزا برای آن در نظر میگیرند و در هنگام ایجاد و ویرایش آن را بر اساس فیلد اصلی مقداردهی و ذخیره می‌کنند.
public class Product
{
    //...
    public string  Title { get; set; }     
    public string  TitleSlug { get; set; } // productAddModel.TitleSlug = SlugMethod(productAddModel.Title);
}
مطالب
صفحه بندی اطلاعات در ASP.NET MVC به روش HashChange
یکی از مواردی که درپروژه‌‌ها زیاد مورد استفاده قرار میگیرد، نمایش داده‌های ذخیره شده‌ی در بانک اطلاعاتی، به صورت صفحه بندی شده به کاربر می‌باشد. قبلا در زمینه بحث Paging، مطلبی تهیه شده بود و در این مقاله قصد داریم کتابخانه‌ای را مورد بررسی قرار دهیم که علاوه بر ارسال داده به صورت Ajax ایی، بتواند همچنین پارامترهای مورد نظر را به صورت Query String نیز در آدرس بار نمایش دهد.
اگر به جستجوی گوگل دقت کرده باشید، به صورت Ajax ایی پیاده سازی شده‌است، با این تفاوت که بعد از هر تغییر درجستجوی مورد نظر، Url صفحه نیز تغییر میکند (برای مثال بعد از جستجوی عبارت dotNetTips  آدرس بار صفحه به شکل https://www.google.com/#q=dotNetTips&* تغییر می‌کند). برای پیاده سازی این ویژگی باید از تکنیکی به نام HashChange استفاده کرد. در نتیجه با این روش مشکل ارسال صفحه‌ای خاص در یک گرید برای دیگران، به صورت Ajax ایی و بدون مشکل انجام می‌شود. از این رو با توجه به داشتن Url‌های منحصر به فرد برای هر صفحه، تا حدی مشکل سئو سایت را نیز برطرف می‌کنیم.

برای استفاده از این ویژگی در ادامه قصد داریم پیاده سازی کتابخانه‌ی MvcAjaxPager را مورد بررسی قرار دهیم. ابتدا قبل از هر کاری، با استفاده از دستور زیر اقدام به نصب کتابخانه آن می‌نماییم:
 Install-Package MvcAjaxPager

در ادامه نحوه پیاده سازی آن را به همراه مثالی، مورد بررسی قرار می‌دهیم:

ابتدا یک مدل فرضی را همانند زیر تهیه می‌کنیم :
public class Topic
{
   public int Id;
   public string Title;
   public string Text;
}
و کلاسی را همانند زیر برای دریافت یک لیست از مطالب می‌نویسیم:
public class TopicService
{
    public static IEnumerable<Topic> Topics = new List<Topic>() {
       new Topic{Id=1,Title="Title 1",Text= "Text 1"},
       new Topic{Id=2,Title="Title 2",Text="Text 2"},
       new Topic{Id=3,Title="Title 3",Text="Text 3"},
       new Topic{Id=4,Title="Title 4",Text="Text 4"},
       new Topic{Id=5,Title="Title 5",Text="Text 5"},
       new Topic{Id=6,Title="Title 6",Text="Text 6"},
       new Topic{Id=7,Title="Title 7",Text="Text 7"},
       new Topic{Id=8,Title="Title 8",Text="Text 8"},
       new Topic{Id=9,Title="Title 9",Text="Text 9"},
       new Topic{Id=10,Title="Title 10",Text="Text 10"},
       new Topic{Id=11,Title="Title 11",Text="Text 11"},
       new Topic{Id=12,Title="Title 12",Text="Text 12"},
       new Topic{Id=13,Title="Title 13",Text="Text 13"},
       new Topic{Id=14,Title="Title 14",Text="Text 14"},
       new Topic{Id=15,Title="Title 15",Text="Text 15"},
       new Topic{Id=16,Title="Title 16",Text="Text 16"},
       new Topic{Id=17,Title="Title 17",Text="Text 17"},
       new Topic{Id=18,Title="Title 18",Text="Text 18"},
       new Topic{Id=19,Title="Title 19",Text="Text 19"},
       new Topic{Id=20,Title="Title 20",Text="Text 20"},
       new Topic{Id=21,Title="Title 21",Text="Text 21"},
       new Topic{Id=22,Title="Title 22",Text="Text 22"},
      };

    public static IEnumerable<Topic> GetAll()
    {
       return Topics.OrderBy(row => row.Id);
    }
}
همچنین کلاس زیر را اضافه میکنیم:
public class ListViewModel
{
   public IEnumerable<Topic> Topics { get; set; }
   public int PageIndex { get; set; }
   public int TotalItemCount { get; set; }
}
ابتدا یک کنترلر را ایجاد می‌کنیم به همراه اکشن متدی که قصد داریم لیستی از اطلاعات را به کاربر نمایش دهیم:
public ActionResult Index(int page = 1)
{
       var topics = TopicService.GetAll ();
       int totalItemCount = topics.Count();
       var model = new ListViewModel()
       {
              PageIndex = page,
              Topics = topics.OrderBy(p => p.Id).Skip((page - 1) * 10).Take(10).ToList(),
              TotalItemCount = totalItemCount
       };

       if (!Request.IsAjaxRequest())
       {
              return View(model);
       }

       return PartialView("_TopicList", model);
}
در اینجا بعد از واکشی اطلاعات، تعداد 10 رکورد را در هر صفحه نمایش می‌دهیم. 

و در Partial view مربوطه نیز داریم :
@using MvcAjaxPager
@model ListViewModel

@Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions
   {
       ShowDisabledPagerItems = true,
       AlwaysShowFirstLastPageNumber = true,
       HorizontalAlign = "center",
       ShowFirstLast = false,
       CssClass = "NavigationBox",
       AjaxUpdateTargetId = "dvTopics",
       AjaxOnBegin = "AjaxStart",
       AjaxOnComplete = "AjaxStop"
   }, null, null)

<table>
    <tr>
        <th>
            @Html.DisplayName("ID")
        </th>
        <th>
            @Html.DisplayName("Title")
        </th>
        <th>
            @Html.DisplayName("Text")
        </th>
    </tr>

    @foreach (var topic in Model.Topics)
    {
        <tr>
            <td>
                @topic.Id
        </td>
        <td>
            @topic.Title
        </td>
        <td>
            @topic.Text
        </td>
    </tr>
    }
</table>

@Html.AjaxPager(Model.TotalItemCount, 10, Model.PageIndex, "Index", "Home", null, new PagerOptions
   {
       ShowDisabledPagerItems = true,
       AlwaysShowFirstLastPageNumber = true,
       HorizontalAlign = "center",
       ShowFirstLast = true,
       FirstPageText = "اولین",
       LastPageText = "آخرین",
       MorePageText = "...",
       PrevPageText = "قبلی",
       NextPageText = "بعدی",
       CssClass = "NavigationBox",
       AjaxUpdateTargetId = "dvTopics",
       AjaxOnBegin = "AjaxStart",
       AjaxOnComplete = "AjaxStop"
   }, null, null)

 حال برای استفاده از pager مورد نظر فقط کافیست متد AjaxPager آن را فراخوانی کنیم. این متد شامل 11  OverLoad مختلف هست.
در این قسمت TotalItemCount جمع کل رکورد‌ها، PageSize تعداد رکورد‌های هر صفحه و PageIndex آدرس صفحه جاری می‌باشد.

مهمترین بخش این pager  که قابلیت‌های زیادی را به کاربر می‌دهد، قسمت PagerOptions آن است و تعدادی از پارامتر‌های آن شامل AjaxOnBeginAjaxOnCompelte، AjaxOnSuccess ،  AjaxOnFailure میتوان تعیین کرد تا بعد از شروع، وقوع خطا، موفقیت و یا خاتمه عملیات جاوا اسکریپتی، اجرا شود. 

AlwaysShowFirstLastPageNumber جهت نمایش صفحه اول و آخر
FirstPageText جهت تعیین متن اولین صفحه
LastPageText جهت تعیین متن آخرین صفحه
CssClass ، Id  جهت تعیین Id خاص

و در انتها، در view مربوطه داریم:
@using MvcAjaxPager
@model ListViewModel
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div id="dvTopics">
        @{
            @Html.Partial("_TopicList", Model);
        }
    </div>

    <script type="text/javascript" src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/Scripts/path.min.js")"></script>
    <script type="text/javascript" src="@Url.Content("~/Scripts/jquery.pager-1.0.1.min.js")"></script>
    <script type="text/javascript">
        $('.NavigationBox').pager();

        //pagination before start
        function AjaxStart() {
            console.log('Start AJAX call. Loading message can be shown');
        }
        // pagination - after request
        function AjaxStop() {
            console.log('Stop AJAX call. Loading message can be hidden');
        };
    </script>
</body>
</html>
در انتهای صفحه مورد نظر می‌بایست دو فایل جاوااسکریپتی jquerypager و Path را که هنگام نصب Pager، به برنامه اضافه شده اند، فراخوانی کنیم و با استفاده از CssClass  یا Id که قبلا در بخش PagerOption تعیین کردیم، آن را انتخاب و متدpager را فراخوانی کنیم.
مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت اول - ایجاد و به روز رسانی جدول مجازی FTS
SQLite به صورت توکار از full-text search پشتیبانی می‌کند؛ اما اهمیت آن چیست؟ هدف از full-text search، انجام جستجوهای بسیار سریع، در ستون‌های متنی یک جدول بانک اطلاعاتی است. بدون وجود یک چنین قابلیتی، عموما برای انجام اینکار از دستور LIKE استفاده می‌شود:
SELECT Title FROM Book WHERE Desc LIKE '%cat%';
کار این کوئری، یافتن ردیف‌هایی است که در آن واژه‌ی cat وجود دارند. مشکل این روش، عدم استفاده‌ی از ایندکس‌ها و اصطلاحا انجام یک full table scan است. با استفاده از دستور LIKE، باید تک تک ردیف‌های بانک اطلاعاتی برای یافتن واژه‌ی مدنظر، اسکن و بررسی شوند و انجام اینکار با بالا رفتن تعداد رکوردهای بانک اطلاعاتی، کندتر و کندتر خواهد شد. برای رفع این مشکل، راه حلی به نام full-text search ارائه شده‌است که کار آن ایندکس کردن تمام ستون‌های متنی مدنظر و سپس جستجوی بر روی این ایندکس از پیش آماده شده‌است.
معادل دستور LIKE در کوئری فوق، متد Contains در EF Core است:
var cats = context.Chapters.Where(item => item.Text.Contains("cat")).ToList();
بنابراین هدف از این سری، جایگزین کردن متدهای الحاقی Contains ، StartsWith و EndsWith، با روشی بسیار سریعتر است.

یک نکته: کوئری فوق توسط EF Core و به همراه پروایدر SQLite آن، به صورت زیر ترجمه می‌شود (که آن نیز یک full table scan است):
SELECT  "c"."Text" FROM "Chapters" AS "c" WHERE ('cat' = '') OR (instr("c"."Text", 'cat') > 0)
اما دقیقا دستور Like را به همراه متدهای الحاقی StartsWith و یا EndsWith می‌توان مشاهده کرد:
var cats = context.Chapters.Where(item => item.Text.StartsWith("cat")).ToList();
// SELECT "c"."Text", FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE 'cat%')
var cats = context.Chapters.Where(item => item.Text.EndsWith("cat")).ToList();
// SELECT "c"."Title" FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE '%cat')


معرفی موجودیت‌های مثال این سری

هدف اصلی ما، ایندکس کردن full-text ستون‌های متنی عنوان و متن جدول بانک اطلاعاتی متناظر با Chapter است:
using System.Collections.Generic;

namespace EFCoreSQLiteFTS.Entities
{
    public class User
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<Chapter> Chapters { get; set; }
    }

    public class Chapter
    {
        public int Id { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }

        public User User { get; set; }
        public int UserId { get; set; }
    }
}


ایجاد جدول مجازی Full-text search

زمانیکه عملیات Migration را در EF Core فعال و اجرا می‌کنیم، دو جدول متناظر با Chapter و User ایجاد می‌شوند. اما برای کار با full-text search، نیاز به ایجاد جداول دیگری است، تا کار نگهداری ایندکس‌های تشکیل شده‌ی از ستون‌های متنی مدنظر ما را انجام دهند. به این نوع جداول در SQLite، جدول مجازی و یا virtual table گفته می‌شود. یک virtual table در اصل تفاوتی با یک جدول معمولی ندارد. تفاوت در اینجا است که منطق دسترسی به این جدول مجازی از موتور FTS5 مربوط به SQLite باید عبور کند. تاکنون نگارش‌های مختلفی از موتور full-text search آن منتشر شده‌اند؛ مانند FTS3 ، FTS4 و غیره که آخرین نگارش آن، FTS5 می‌باشد و به همراه توزیعی که مایکروسافت ارائه می‌دهد، وجود دارد و نیازی به تنظیمات خاصی ندارد.
در اینجا روش ایجاد یک جدول مجازی جدید Chapters_FTS را مشاهده می‌کنید:
CREATE VIRTUAL TABLE "Chapters_FTS"
USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
جدول مجازی، با اجرای دستور CREATE VIRTUAL TABLE  ایجاد می‌شود و USING fts5 آن به معنای استفاده‌ی از موتور full-text search نگارش پنجم آن است. سپس لیست ستون‌هایی را که می‌خواهیم ایندکس کنیم، ذکر می‌شوند؛ مانند Text و Title در اینجا. همانطور که مشاهده می‌کنید، فقط نام این ستون‌ها قابل تعریف هستند و هیچ نوع اطلاعات اضافه‌تری را نمی‌توان ذکر کرد.
ذکر پارامتر "content="Chapters اختیاری بوده و به این معنا است که نیازی نیست تا اصل داده‌های مرتبط با ستون‌های ذکر شده نیز ذخیره شوند و آن‌ها را می‌توان از جدول Chapters، بازیابی کرد. در این حالت برای برقراری ارتباط بین این جدول مجازی و جدول chapters، پارامتر "content_rowid="Id مقدار دهی شده‌است. content_rowid به primary key جدول content اشاره می‌کند. ذکر هر دوی این پارامترها اختیاری بوده و در صورت تنظیم، حجم نهایی بانک اطلاعاتی را کاهش می‌دهند. چون در این حالت دیگری نیازی به ذخیره سازی جداگانه‌ی اصل اطلاعات متناظر با ایندکس‌های FTS نیست.

اکنون که با دستور ایجاد جدول مجازی FTS آشنا شدیم، روش ایجاد آن در برنامه‌های مبتنی بر EF Core نیز دقیقا به همین صورت است:
private static void createFtsTables(ApplicationDbContext context)
{
    // For SQLite FTS
    // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too.
    context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS""
    USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");");
}
فقط کافی است در ابتدای اجرای برنامه با استفاده از متد ExecuteSqlRaw، عبارت SQL متناظر با ایجاد جدول مجازی را اجرا کنیم. این یک روش ایجاد این نوع جداول است؛ روش دیگر آن، قرار دادن همین قطعه کد در متد "protected override void Up(MigrationBuilder migrationBuilder)" مربوط به کلاس‌های ایجاد شده‌ی توسط عملیات Migration است.


به روز رسانی اطلاعات جدول مجازی FTS، توسط تریگرها

پس از اجرای دستورCREATE VIRTUAL TABLE  فوق، SQLite پنج جدول را به صورت خودکار ایجاد می‌کند که در تصویر زیر قابل مشاهده هستند:


البته ما مستقیما با این جداول کار نخواهیم کرد و این جداول برای نگهداری اطلاعات ایندکس‌های full-text موتور FTS5، توسط خود SQLite نگهداری و مدیریت می‌شوند.

اما ... نکته‌ی مهم اینجا است که جدول مجازی Chapters_FTS، هرچند به جدول اصلی Chapters توسط پارامتر content آن متصل شده‌است، اما تغییرات آن‌را ردیابی نمی‌کند. یعنی هر نوع insert/update/delete ای که در جدول اصلی Chapters رخ می‌دهد، سبب ایندکس شدن اطلاعات جدید آن در جدول مجازی Chapters_FTS نمی‌شود و برای اینکار باید اطلاعات را مستقیما در جدول Chapters_FTS درج کرد.
روش پیشنهاد شده‌ی در مستندات رسمی آن، استفاده از تریگرهای پس از درج اطلاعات، پس از حذف اطلاعات و پس از به روز رسانی اطلاعات به صورت زیر است:
-- Create a table. And an external content fts5 table to index it.
CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a');

-- Triggers to keep the FTS index up to date.
CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
END;
CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
در اینجا ابتدا روش ایجاد یک جدول جدید و سپس ایجاد یک جدول مجازی FTS را از روی آن مشاهده می‌کنید.
در ادامه سه تریگر بر روی جدول اصلی که ما به صورت متداولی با آن در برنامه‌های خود کار می‌کنیم، تعریف شده‌اند. این تریگرها کار insert اطلاعات را در جدول مجازی ایجاد شده، به صورت خودکار انجام می‌دهند.
همانطور که مشاهده می‌کنید، یک rowid نیز در اینجا قابل تعریف است؛ rowid، ستون مخفی یک جدول مجازی FTS است و هرچند در حین ایجاد، آن‌را ذکر نمی‌کنیم، اما جزئی از ساختار آن بوده و قابل کوئری گرفتن است.

نکته‌ی مهم: به فرمت دستورات به روز رسانی جدول مجازی FTS دقت کنید. حتی در حالت تریگرهای update و یا delete نیز در اینجا دستور insert، مشاهده می‌شوند. این فرمت دقیقا باید به همین نحو رعایت شود؛ در غیراینصورت اگر از دستورات delete و یا update معمولی بر روی این جدول مجازی استفاده کنید، دفعه‌ی بعدی که برنامه را اجرا می‌کنید، خطای «این بانک اطلاعاتی تخریب شده‌است» را مشاهده کرده (database disk image is malformed) و دیگر نمی‌توانید با فایل بانک اطلاعاتی خود کار کنید.


به روز رسانی اطلاعات جدول مجازی FTS توسط EF Core

روش تعریف تریگرهای یاد شده، مستقل از EF Core بوده و راسا توسط خود بانک اطلاعاتی مدیریت می‌شود. بنابراین فقط کافی است دستور CREATE TRIGGER را به همان نحوی که عنوان شد، توسط متد ExecuteSqlRaw اجرا کنیم تا جزئی از ساختار بانک اطلاعاتی شوند؛ اما ... این روش برای برنامه‌هایی با متن‌های پیچیده کارآیی ندارد. برای مثال فرض کنید اطلاعات اصلی شما با فرمت HTML است. ایندکس ایجاد شده، تگ‌های HTML را حذف نمی‌کند و آن‌ها را نیز ایندکس می‌کند که نه تنها سبب بالا رفتن حجم بانک اطلاعاتی می‌شود، بلکه زمانیکه ما قصد جستجویی را بر روی اطلاعات HTML ای داریم، اساسا کاری به تگ‌های آن نداشته و هدف اصلی ما، متن‌های درج شده‌ی در آن است. نمونه‌ی دیگر آن داشتن اطلاعاتی با «اعراب» است و یا شاید نیاز به یک‌دست سازی ی و ک فارسی وجود داشته باشد. به این نوع عملیات، «نرمال سازی متن» گفته می‌شود و با روش تریگرهای فوق قابل تعریف و مدیریت نیست. به همین جهت می‌توان از روش پیشنهادی زیر استفاده کرد:

الف) یافتن لیست اطلاعات تغییر یافته‌ی حاصل از اعمال insert/update/delete
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class EFChangeTrackerExtensions
    {
        public static List<(EntityState State, TEntity NewEntity, TEntity OldEntity)>
                    GetChangedEntities<TEntity>(this DbContext dbContext) where TEntity : class, new()
        {
            if (!dbContext.ChangeTracker.AutoDetectChangesEnabled)
            {
                // ChangeTracker.Entries() only calls `Try`DetectChanges() behind the scene.
                dbContext.ChangeTracker.DetectChanges();
            }

            return dbContext.ChangeTracker.Entries<TEntity>()
                    .Where(IsEntityChanged)
                    .Select(entityEntry => (entityEntry.State,
                                            entityEntry.Entity,
                                            createWithValues<TEntity>(entityEntry.OriginalValues)))
                    .ToList();
        }

        private static bool IsEntityChanged(EntityEntry entry)
        {
            return entry.State == EntityState.Added
                    || entry.State == EntityState.Modified
                    || entry.State == EntityState.Deleted
                    || entry.References.Any(r => r.TargetEntry?.Metadata.IsOwned() == true && IsEntityChanged(r.TargetEntry));
        }

        private static T createWithValues<T>(PropertyValues values) where T : new()
        {
            var entity = new T();
            foreach (var prop in values.Properties)
            {
                var value = values[prop.Name];
                if (value is PropertyValues)
                {
                    throw new NotSupportedException("nested complex object");
                }
                else
                {
                    prop.PropertyInfo.SetValue(entity, value);
                }
            }
            return entity;
        }
    }
}
هدف از متد GetChangedEntities فوق این است که با استفاده از سیستم tracking، نوع عملیات انجام شده و همچنین اصل موجودیت‌ها را پیش و پس از تغییر، بتوان لیست کرد و سپس بر اساس آن‌ها، جدول مجازی FTS را به روز رسانی نمود.
علت نیاز به نمونه‌ی اصل و سپس تغییر کرده‌ی موجودیت‌ها، به نحوه‌ی تعریف تریگرهای مخصوص به به روز رسانی FTS بر می‌گردد. اگر دقت کرده باشید در این تریگرها، new.a و همچنین old.a را داریم که برای شبیه سازی آن‌ها دقیقا باید به اطلاعات یک رکورد، در پیش و پس از به روز رسانی آن، دسترسی یافت.

ب) تعریف تریگرهای SQL توسط سیستم tracking؛ به همراه عملیات نرمال سازی اطلاعات
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using EFCoreSQLiteFTS.Entities;
using Microsoft.EntityFrameworkCore;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class FtsNormalizer
    {
        private static readonly Regex _htmlRegex = new Regex("<[^>]*>", RegexOptions.Compiled);

        public static string NormalizeText(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                return string.Empty;
            }

            // Remove html tags
            text = _htmlRegex.Replace(text, string.Empty);

            // TODO: add other normalizers here, such as `remove diacritics`, `fix Persian Ye-Ke` and so on ...

            return text;
        }
    }

    public static class UpdateFtsTriggers
    {
        public static void UpdateChapterFTS(
            this DbContext context,
            List<(EntityState State, Chapter NewEntity, Chapter OldEntity)> changedChapters)
        {
            var database = context.Database;

            try
            {
                database.BeginTransaction(IsolationLevel.ReadCommitted);

                foreach (var (State, NewEntity, OldEntity) in changedChapters)
                {
                    var chapterNew = NewEntity;
                    var chapterOld = OldEntity;

                    var normalizedNewText = chapterNew.Text.NormalizeText();
                    var normalizedOldText = chapterOld.Text.NormalizeText();
                    var normalizedNewTitle = chapterNew.Title.NormalizeText();
                    var normalizedOldTitle = chapterOld.Title.NormalizeText();
                    switch (State)
                    {
                        case EntityState.Added:
                            if (shouldSkipAddedChapter(chapterNew))
                            {
                                continue;
                            }
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Modified:
                            if (shouldSkipModifiedChapter(chapterNew, chapterOld))
                            {
                                continue;
                            }
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                                        chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Deleted:
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                    chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            break;
                    }
                }
            }
            finally
            {
                database.CommitTransaction();
            }
        }

        private static bool shouldSkipAddedChapter(Chapter chapterNew)
        {
            // TODO: add your logic to avoid indexing this item
            return false;
        }

        private static bool shouldSkipModifiedChapter(Chapter chapterNew, Chapter chapterOld)
        {
            // TODO: add your logic to avoid indexing this item
            return chapterNew.Text == chapterOld.Text && chapterNew.Title == chapterOld.Title;
        }
    }
}
در اینجا نحوه‌ی تعریف متد UpdateChapterFTS را مشاهده می‌کند که اطلاعات خودش را از متد GetChangedEntities دریافت کرده و سپس یکی یکی آن‌ها را در جدول مجازی FTS، با فرمت مخصوصی که عنوان شد (دقیقا متناظر با فرمت تریگرهای مستندات رسمی FTS)، درج می‌کند.
همچنین در اینجا متد NormalizeText را نیز مشاهده می‌کند که بر روی ستون‌های متنی اعمال شده‌است. کار آن پاکسازی تگ‌های یک متن HTML ای است و نگهداری اطلاعات صرفا متنی آن. در اینجا اگر نیاز بود می‌توان منطق‌های پاکسازی اطلاعات دیگری را نیز اعمال کرد.
اکنون که این اطلاعات به صورت پاکسازی شده در جدول مجازی درج می‌شوند، زمانیکه بر روی آن‌ها جستجویی صورت می‌گیرد، دیگر شامل جستجوی بر روی تگ‌های HTML ای نیست و دقت بسیار بیشتری دارد.

ج) اتصال به سیستم
پس از تعریف متدهای الحاقی GetChangedEntities و UpdateChapterFTS، اکنون روش اتصال آن‌ها به DbContext برنامه، با بازنویسی متد SaveChanges آن است:
namespace EFCoreSQLiteFTS.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Chapter> Chapters { get; set; }
        public DbSet<User> Users { get; set; }

        public override int SaveChanges()
        {
            var changedChapters = this.GetChangedEntities<Chapter>();

            this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges();
            this.ChangeTracker.AutoDetectChangesEnabled = true;

            this.UpdateChapterFTS(changedChapters);
            return result;
        }
    }
}
از این پس تمام عملیات insert/update/delete برنامه تحت کنترل قرار گرفته و به صورت خودکار سبب به روز رسانی جدول مجازی FTS نیز می‌شوند.


در قسمت بعدی، روش کوئری گرفتن از این جدول مجازی FTS را بررسی می‌کنیم.
مطالب
آشنایی با WPF قسمت سوم: Layouts بخش دوم

  در مقاله قبلی در مورد تعدادی از Layout‌ها صحبت کردیم و در این بخش به ادامه‌ی آن پرداخته و دو مبحث GridPanel و Custom Layout را بررسی می‌کنیم.


GridPanel

پنل پیش فرضی است که موقع ایجاد یک پروژه‌ جدید WPF ایجاد می‌شود. چیدمان این نوع پنل به صورت سطر و ستون است و کارکرد آن بسیار مشابه جداول در HTML می‌باشد؛ با این تفاوت که در اینجا انعطاف پذیری بیشتری وجود دارد. هر سلول می‌تواند شامل چندین کنترل شود و یا هر کنترل می‌تواند چندین سلول را به خود احتصاص دهند و حتی می‌تواند روی کنترل‌های دیگر قرار بگیرند و همپوشانی کنترل‌ها را داشته باشیم.

تگ Grid Panel شامل دو تگ برای تعریف سطرها و ستون‌ها می‌باشد با استفاده از تگ Row Definition و Column Definition به تعیین تعداد سطر و ستون‌ها و اندازه آن‌ها می‌پردازیم:
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="28" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="200" />
    </Grid.ColumnDefinitions>
</Grid>
گرید پنل بالا شامل 4 سطر و دو ستون است و تعیین اندازه آن‌ها توسط دو خاصیت Width و  Height مشخص شده است که نحوه مقداردهی آن‌ها به صورت زیر است:
Fixed : یک مقدار ثابت، مثل سطر آخری که در کد بالا قرار می‌گیرد. این مقدار بر اساس یک واحد منطقی است و نه پیکسل که در این مقاله قبلا بررسی کرده‌ایم.
Auto : به مقداری که احتیاج دارد فضایی را بخود اختصاص می‌دهد.
* : هر آنچه از فضای موجود باقی مانده است را به خود اختصاص می‌دهد. علامت ستاره یک واحد نسبی است؛ به این صورت که می‌توانید مقدار فضا را به صورت زیر نیز بیان کنید.*3 و *2 به این معنی است که از پنج قسمت فضای باقیمانده سه قسمت و بعدی دو قسمت  را به خود اختصاص می‌دهد. عبارت * با *1 برابر است. عموما با این علامت فضا را به شکل درصد بیان می‌کنند:
 <ColumnDefinition Width="69*" />   <!-- Take 69% of remainder -->
    <ColumnDefinition Width="31*"/> <!-- Take 31% of remainder -->

نحوه‌ی اضافه کردنالمان‌ها به گرید به صورت زیر پس از تعیین تعداد سطرها و ستون‌ها انجام می‌گیرد و جایگاه هر المان در ستون یا سطر مربوطه توسط یک attached Dependency Property به نام‌های Grid.Column یا Grid.Row صورت می‌گیرد. خصوصیات Horizontal alignment و vertical Alignment هم برای تعیین موقعیت قرار گیری اشیاء در سلول به کار می‌روند و فاصله‌ی آن‌ها (کنترل ها) از لبه‌های گرید با margin محاسبه می‌شود.
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
        <RowDefinition Height="28" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="200" />
    </Grid.ColumnDefinitions>
    <Label Grid.Row="0" Grid.Column="0" Content="Name:"/>
    <Label Grid.Row="1" Grid.Column="0" Content="E-Mail:"/>
    <Label Grid.Row="2" Grid.Column="0" Content="Comment:"/>
    <TextBox Grid.Column="1" Grid.Row="0" Margin="3" />
    <TextBox Grid.Column="1" Grid.Row="1" Margin="3" />
    <TextBox Grid.Column="1" Grid.Row="2" Margin="3" />
    <Button Grid.Column="1" Grid.Row="3" HorizontalAlignment="Right" 
            MinWidth="80" Margin="3" Content="Send"  />
</Grid>

تغییر اندازه در سمت کد هم می‌تواند توسط کدهای صورت گیرد.
Auto sized GridLength.Auto
Star sized new GridLength(1,GridUnitType.Star)
Fixed size new GridLength(100,GridUnitType.Pixel)
مثال:
Grid grid = new Grid();
 
ColumnDefinition col1 = new ColumnDefinition();
col1.Width = GridLength.Auto;
ColumnDefinition col2 = new ColumnDefinition();
col2.Width = new GridLength(1,GridUnitType.Star);
 
grid.ColumnDefinitions.Add(col1);
grid.ColumnDefinitions.Add(col2);

قابلیت تغییر اندازه‌ی سطر و ستون توسط کاربر
یکی از تگ‌های ویژه داخل گری،د تگ Grid Splitter است. برای قرارگیری تگ splitter ابتدا باید یک سطر یا ستون بین سطر و ستون هایی که میخواهید از یکدیگر جدا شوند ایجاد کنید و اندازه‌ی آن را auto تعیین کنید و سپس مانند بقیه‌ی اشیا توسط Grid.Column یا Grid.Row مانند کد زیر تگ splitter را به آن اختصاص دهید.
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Label Content="Left" Grid.Column="0" />
    <GridSplitter HorizontalAlignment="Right" 
                  VerticalAlignment="Stretch" 
                  Grid.Column="1" ResizeBehavior="PreviousAndNext"
                  Width="5" Background="#FFBCBCBC"/>
    <Label Content="Right" Grid.Column="2" />
</Grid>
خاصیت ResizeBehavior مشخص می‌کند که ستون یا سطرهای کناری کدام باید تغییر اندازه داشته باشند.
 BasedOnAlignment   مقدار پیش فرض این گزینه است و مشخص می‌کند سطر یا ستونی طرفی باید تغییر اندازه دهد که در Alignment آن آمده است
 CurrentAndNext   ستون یا سطر جاری  به همراه ستون یا سطر بعدی
 PreviousAndCurrent   ستون یا سطر جاری  به همراه ستون یا سطر قبلی
 PreviousAndNext   سطر یا ستون قبلی و بعدی که بهترین گزینه برای انتخاب است.

خاصیت ResizeDirection جهت تغییر اندازه را مشخص می‌کند که شامل سه مقدار Row,Column و Auto است که مقدار پیش فرض آن auto است و نیازی به ذکر آن نیست و خود سیستم میداند که باید تغییر اندازه در چه جهتی صورت بگیرد.


ساخت Custom Layout یا یک پنل سفارشی (اختصاصی)
در این دو قسمت، شما با پنل‌های متفاوتی آشنا شدید که قابلیت‌های مفیدی داشتند؛ ولی گاهی اوقات هیچ کدام از این‌ها به کار شما نمی‌آیند و دوست دارید پنلی داشته باشید که مطابق میل شما عمل کند. برای ساخت یک پنل سفارشی یک کلاس می‌سازیم که از کلاس Panel ارث بری می‌کند. در اینجا دو متد برای Override کردن وجود دارند:
MeasureOverride : تعیین اندازه پنل بر اساس اندازه تعیین شده برای المان‌های فرزند و فضای موجود.
ArrangeOverride: مرتب سازی المان‌ها در فضای موجود نهایی.

کد نمونه:
public class MySimplePanel : Panel
{
    // Make the panel as big as the biggest element
    protected override Size MeasureOverride(Size availableSize)
    {
        Size maxSize = new Size();
 
        foreach( UIElement child in InternalChildern)
        {
            child.Measure( availableSize );
            maxSize.Height = Math.Max( child.DesiredSize.Height, maxSize.Height);
            maxSize.Width= Math.Max( child.DesiredSize.Width, maxSize.Width);
        }
    }
 
    // Arrange the child elements to their final position
    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach( UIElement child in InternalChildern)
        {
            child.Arrange( new Rect( finalSize ) );
        }
    }
}
لینک‌های زیر تعدادی از پنل‌های سفارشی پر طرفدار هستند که بر روی اینترنت به اشتراک گذاشته شده اند:
TreeMapPanel
Animating Tile Panel
Radial Panel
Element Flow Panel
Ribbon Panel

خواصی که باید در Layout‌ها با آنها بیشتر آشنا شویم:
Horizontal & Vertical Alignment
با دادن این خاصیت به کنترل‌های موجود، نحوه قرار گیری و موقعیت آن‌ها مشخص می‌گردد. جدول زیر بر ساس انواع موقعیت‌های مختلف تشکیل شده است:

Margin & Padding
این خاصیت‌ها حتما برای شما آشنا هستند. خاصیت margin فاصله کنترل از لبه‌های Layout است و خاصیت Padding فاصله محتویات کنترل از لبه‌های کنترل است.

Clipping
در صورتی که خاصیت ClipToBounds پنل برابر False باشند به این معناست که المان‌ها میتوانند از لبه‌های پنل خارج شوند، در صورتی که برابر True باشد مقدار خارج شده نمایش نمی‌یابد.

Scrolling
موقعیکه از پنلی استفاده می‌کنید که با تمام شدن ناحیه‌اش روبرو شده‌اید ولی کنترل‌های داخلش هنوز ادامه دارند، نیاز به یک اسکرول به شدت احساس می‌شود. در این حالت می‌توان از ScrollViewer استفاده کرد.
<ScrollViewer>
    <StackPanel>
        <Button Content="First Item" />
        <Button Content="Second Item" />
        <Button Content="Third Item" />
    </StackPanel>
</ScrollViewer>



مطالب
بازسازی کد: جایگزینی آرایه با شیء (Replace array with object)
از آرایه برای ذخیره سازی آیتم‌های مشابه استفاده می‌شود. این تشابه باید علاوه بر اینکه در نوع داده‌ای آیتم‌ها رعایت شود، باید از نظر مفهومی نیز رعایت شود.
زمانیکه از یک آرایه برای نگهداری المنت‌های غیر مشابه استفاده می‌شود، نیاز به چنین بازسازی کدی است. به طور مثال آرایه‌ای که آیتم اول آن "نام" و آیتم دوم آن "امتیاز" است. قطعا کار با چنین آرایه‌ای بسیار مشکل خواهد بود. زمانیکه یک آرایه را از نوع داده‌ای عمومی‌تری (مثلا object در سی شارپ) تعریف و انواع داده‌ای متفاوت را در آیتم‌های آن نگهداری کنیم، اوضاع بسیار بدتر خواهد شد. 
محور اصلی بازسازی کد "جایگزینی آرایه با شیء" ایجاد یک کلاس، برای ذخیره اطلاعات آرایه است. به این صورت که برای هر آیتم آرایه، یک خصوصیت در کلاس مربوطه ایجاد می‌شود. 
به طور مثال به آرایه زیر توجه نمایید:
var row = new string[3]; 
row[0] = "Liverpool"; 
row[0] = "15";
در آرایه بالا، آیتم اول نشان دهنده نام تیم و آیتم دوم نشان دهنده امتیاز تیم است. با وجود اینکه این مثال کمی غیر واقعی به نظر میرسد، اما چنین مثال‌هایی در برنامه نویسی روزمره ممکن است به اشکال مختلفی مشاهده شود. مانند استفاده از dictionary برای دریافت اطلاعات فرم وب، استفاده از Tuple (در زبان سی شارپ) برای انتقال اطلاعات و … 
در این مثال طراحی بهتر، ایجاد یک کلاس یا ساختار (بسته به شرایط کلی مسئله) برای نشان دادن امتیاز تیم است:  
public class Performance 
{ 
       public string TeamName { get; set; } 
       public int Score { get; set; } 
}
همانطور که مشاهده می‌کنید، به ازای هر یک از آیتم‌های آرایه، خصوصیتی در کلاس جدید ایجاد شده‌است. همچنین انتخاب انواع داده‌ای نیز در طراحی جدید، ساده‌تر و اصولی‌تر انجام خواهد شد.
تمامی استفاده‌ها از آرایه‌ها، در دسته بندی این نوشتار برای بازسازی کد قرار نمی‌گیرند. آرایه‌هایی که اصل مشابه بودن آیتم‌ها را رعایت می‌کنند، معمولا نیازی به بازسازی کد ندارند. به طور مثال در نرم افزارهای فروشگاه اینترنتی، خصوصیات کالا به صورت داینامیک ذخیره شده و احتمالا برای دسترسی و مدیریت آن، از آرایه یا لیست استفاده می‌شود. اما با کمی دقت خواهیم دید، این استفاده از آرایه، با تعریف مشابه بودن آیتم‌ها همخوانی دارد. زیرا تمامی آیتم‌های آرایه به طور مثال از نوع خصوصیت کالا هستند. همچنین عملا امکان بازسازی و ایجاد کلاس در این مثال وجود ندارد؛ زیرا خصوصیات کالاها در زمان توسعه مشخص نیستند و در زمان اجرای برنامه تنظیم می‌شوند. 
مطالب
StringBuilder

بهترین روش برای تولید و دستکاری یک رشته (string) طولانی و یا دستکاری متناوب و تکراری یک رشته استفاده از کلاس StringBuilder است. این کلاس در فضای نام System.Text قرار داره. شی String در دات‌نت‌فریمورک تغییرناپذیره (immutable)، بدین معنی که پس از ایجاد نمی‌توان محتوای اونو تغییر داد. برای مثال اگر شما بخواین محتوای یک رشته رو با اتصال به رشته‌ای دیگه تغییر بدین، اجازه اینکار را به شما داده نمی‌شه. درعوض به‌صورت خودکار رشته‌ای جدید در حافظه ایجاد میشه و محتوای دو رشته موجود پس از اتصال به هم درون اون قرار می‌گیره. این کار درصورتی‌که تعداد عملیات مشابه زیاد باشه می‌تونه تاثیر منفی بر کارایی و حافظه خالی در دسترس برنامه بگذاره.

کلاس StringBuilder با استفاده از آرایه‌ای از کاراکترها، راه‌حل مناسب و بهینه‌ای رو برای این مشکل فراهم کرده. این کلاس در زمان اجرا به شما اجازه می‌ده تا بدون ایجاد نمونه‌های جدید از کلاس String، محتوای یک رشته رو تغییر بدین. شما می‌تونید نمونه‌ای از این کلاس رو به‌صورت خالی و یا با یک رشته اولیه ایجاد کنید، سپس با استفاده از متدهای متنوع موجود، محتوای رشته رو با استفاده از انواع داده مختلف و به‌صورت دلخواه دستکاری کنید. هم‌چنین با استفاده از متد معروف  ()ToString این کلاس می‌تونید در هر لحظه دلخواه رشته تولیدی رو بدست بیارین. دو پراپرتی مهم کلاس StringBuilder رفتارش رو درهنگام افزودن داده‌های جدید کنترل می‌کنن:

Capacity , Length

پراپرتی Capacity اندازه بافر کلاس StringBuilder را تعیین می‌کنه و Length طول رشته جاری موجود در این بافر رو نمایش می‌ده. اگر پس از افزودن داده جدید، طول رشته از اندازه بافر موجود بیشتر بشه، StringBuilder باید یه بافر جدید با اندازه‌ای مناسب ایجاد کنه تا رشته جدید رو بتونه تو خودش نگه داره. اندازه این بافر جدید به‌صورت پیش‌فرض دو برابر اندازه بافر قبلی درنظر گرفته می‌شه. بعد تمام رشته قبلی رو تو این بافر جدید کپی میکنه.

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

using System.IO;
using System.Text;

class Program
{
  static void Main()
  {
    using (var writer = new StreamWriter("data.txt"))
    {
      var builder = new StringBuilder();
      for (var i = 0; i <= 256; i++)
      {
        writer.Write(builder.Capacity);
        writer.Write(",");
        writer.Write(builder.Length);
        writer.WriteLine();
        builder.Append('1'); // <-- Add one character
      }
    }
  }
}

دقت کنین که برای افزودن یک کاراکتر استفاده از دستور Append با نوع داده char (همونطور که در بالا استفاده شده) بازدهی بهتری نسبت به استفاده از نوع داده string (با یک کاراکتر) داره. خروجی کد فوق به صورت زیره:

16, 0
16, 1
16, 2
...
16,14
16,15
16,16
32,17
...

استفاده نامناسب و بی‌دقت از این کلاس می‌تونه منجر به بازسازی‌های متناوب این بافر شده که درنهایت فواید کلاس StringBuilder رو تحت تاثیر قرار میده. درهنگام کار با کلاس StringBuilder اگر از طول رشته موردنظر و یا حد بالایی برای Capacity آن آگاهی حتی نسبی دارین، می‌تونید با مقداردهی مناسب این پراپرتی از این مشکل پرهیز کنید.

نکته: مقدار پیش‌فرض پراپرتی Capacity برابر 16 است.

هنگام مقداردهی پراپرتی‌های Capacity یا Length به موارد زیر توجه داشته باشید:

- مقداردهی Capacity به میزانی کمتر از طول رشته جاری (پراپرتی Length)، منجر به خطای زیر می‌شه:

System.ArgumentOutOfRangeException

خطای مشابهی هنگام مقداردهی پراپرتی Capacityبه بیش از مقدار پراپرتی MaxCapacity رخ می‌دهه.البته این مورد تنها درصورتی‌که بخواین اونو به بیش از حدود 2 گیگابایت (Int32.MaxValue) مقداردهی کنید پیش میاد!

- اگر پراپرتی Length را به مقداری کمتر از طول رشته جاری تنظیم کنید، رشته به اندازه طول تنظیمی کوتاه (truncate) میشه.

- اگر مقدار پراپرتی Length را به میزانی بیشتر از طول رشته جاری تنظیم کنید، فضای خالی موجود در بافر با space پر میشه.

- تنظیم مقدار Length بیشتر از Capacity، منجر به مقداردهی خودکار پراپرتی Capacity به مقدار جدید تنظیم شده برای Length میشه.

در ادامه به یک مثال برای مقایسه کارایی تولید یک رشته طولانی با استفاده از این کلاس میپردازیم. تو این مثال از دو روش برای تولید رشته‌های طولانی استفاده میشه. روش اول که همون روش اتصال رشته‌ها (Concat) به هم هستش و روش دوم هم که استفاده از کلاس StringBuilder است. در قطعه کد زیر کلاس مربوط به عملیات تست رو مشاهده میکنین:

namespace StringBuilderTest
{
  internal class SbTest1
  {
    internal Action<string> WriteLog;
    internal int Iterations { get; set; }
    internal string TestString { get; set; }

    internal SbTest1(int iterations, string testString, Action<string> writeLog)
    {
      Iterations = iterations;
      TestString = testString;
      WriteLog = writeLog;
    }

    internal void StartTest()
    {
      var watch = new Stopwatch();

      //StringBuilder
      watch.Start();
      var sbTestResult = SbTest();
      watch.Stop();
      WriteLog(string.Format("StringBuilder time: {0}", watch.ElapsedMilliseconds));

      //Concat
      watch.Start();
      var concatTestResult = ConcatTest();
      watch.Stop();
      WriteLog(string.Format("ConcatTest time: {0}", watch.ElapsedMilliseconds));

      WriteLog(string.Format("Results are{0} the same", sbTestResult == concatTestResult ? string.Empty : " NOT"));
    }

    private string SbTest()
    {
      var sb = new StringBuilder(TestString);
      for (var x = 0; x < Iterations; x++)
      {
        sb.Append(TestString);
      }
      return sb.ToString();
    }

    private string ConcatTest()
    {
      string concat = TestString;
      for (var x = 0; x < Iterations; x++)
      {
        concat += TestString;
      }
      return concat;
    }
  }
}

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

do
{
  Console.Write("Iteration: ");
  var iterations = Convert.ToInt32(Console.ReadLine());
  Console.Write("Test String: ");
  var testString = Console.ReadLine();
  var test1 = new SbTest1(iterations, testString, Console.WriteLine);
  test1.StartTest();
  Console.WriteLine("----------------------------------------------------------------");
} while (Console.ReadKey(true).Key == ConsoleKey.C); // C = continue 

برای نمونه خروجی زیر در لپ‌تاپ من (Corei7 2630QM) بدست اومد:

تنظیم خاصیت Capacity به یک مقدار مناسب میتونه تو کارایی تاثیرات زیادی بگذاره. مثلا در مورد مثال فوق میشه یه متد دیگه برای آزمایش تاثیر این مقداردهی به صورت زیر به کلاس برناممون اضافه کنیم:

private string SbCapacityTest()
{
  var sb = new StringBuilder(TestString) { Capacity = TestString.Length * Iterations };
  for (var x = 0; x < Iterations; x++)
  {
    sb.Append(TestString);
  }
  return sb.ToString();
}

تو این متد قبل از ورود به حلقه مقدار خاصیت Capacity به میزان موردنظر تنظیم شده و نتیجه بدست اومده:

مشاهده میشه که روش concat خیلی کنده (دقت کنین که طول رشته اولیه هم بیشتر شده) و برای ادامه کار مقایسه اون رو کامنت کردم و نتایج زیر بدست اومد:

می‌بینین که استفاده مناسب از مقداردهی به خاصیت Capacity میتونه تا حدود 300 درصد سرعت برنامه ما رو افزایش بده. البته همیشه اینطوری نخواهد بود. ما در این مثال مقدار دقیق طول رشته نهایی رو میدونستیم که باعث میشه عملیات افزایش بافر کلاس StringBuilder هیچوقت اتفاق نیفته. این امر در واقعیت کمتر پیش میاد.

مقاله موجود در سایت dotnetperls شکل زیر رو به عنوان نتیجه تست بازدهی ارائه میده:

- در مواقعی که عملیاتی همچون مثال بالا طولانی و حجیم ندارین بهتره که از این کلاس استفاده نکنین چون عملیات‌های داخلی این کلاس در عملیات کوچک و سبک (مثل ابتدای نمودار فوق) موجب کندی عملیات میشه. همچنین استفاده از اون نیاز به کدنویسی بیشتری داره.

- این کلاس فشار کمتری به حافظه سیستم وارد میکنه. درمقابل استفاده از روش concat موجب اشغال بیش از حد حافظه میشه که خودش باعث اجرای بیشتر و متناوب‌تر GC میشه که در نهایت کارایی سیستم رو کاهش میده.

- استفاده از این کلاس برای عملیات Replace (و یا عملیات مشابه) در حلقه‌ها جهت کار با رشته‌های طولانی و یا تعداد زیادی رشته میتونه بسیار سریعتر و بهتر عمل کنه چون این کلاس برخلاف کلاس string اشیای جدید تولید نمیکنه.

- یه اشتباه بزرگ در استفاده از این کلاس استفاده از "+" برای اتصال رشته‌های درون StringBuilder هست. هرگز از این کارها نکنین. (فکر کنم واضحه که چرا)

مطالب
Blazor 5x - قسمت نهم - مبانی Blazor - بخش 6 - ساده سازی تعاریف ویژگی‌های المان‌ها و انتقال پارامترها به چندین زیر سطح
بررسی ویژگی Attribute Splatting

برای تعریف المان‌های فرم‌ها نیاز است ویژگی‌های قابل توجهی را مانند placeholder ،required ،maxlength و غیره، تعریف کرد که در صورت زیاد بودن تعداد المان‌های یک فرم، مدیریت تعریف این ویژگی‌ها مشکل می‌شود. به همین جهت قابلیت ویژه‌ای مخصوص اینکار به نام Attribute Splatting در Blazor درنظر گرفته شده‌است. برای توضیح آن، ابتدا کامپوننت والد Pages\LearnBlazor\AttributeSplatting.razor و کامپوننت فرزند Pages\LearnBlazor\LearnBlazor‍Components\AttributeSplattingChild.razor را ایجاد می‌کنیم.
در کامپوننت فرزند یا همان AttributeSplattingChild، یک المان را به همراه تعدادی ویژگی تعریف شده مشاهده می‌کنید:
<div>
    <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4>

    <input id="roomName"
        placeholder="@Placeholder"
        required="@Required"
        maxlength="@MaxLength"
        class="form-control" />
</div>

@code {
    [Parameter]
    public string Placeholder { get; set; } = "Initial Text";

    [Parameter]
    public string Required { get; set; } = "required";

    [Parameter]
    public string MaxLength { get; set; } = "10";
}
و کامپوننت والد و یا همان AttributeSplatting.razor، از آن به صورت زیر استفاده می‌کند:
@page "/AttributeSplatting"

<h1>Attribute Splatting</h1>

<AttributeSplattingChild
    Placeholder="Enter the Room Name From Parent"
    MaxLength="5">
</AttributeSplattingChild>
روش ارسال پارامترها را به کامپوننت‌های فرزند، در قسمت پنجم این سری بررسی کردیم. تنها نکته‌ی جدید آن، تعریف مقادیر پیش‌فرض پارامترها در کامپوننت فرزند است. برای مثال در حین تعریف المان AttributeSplattingChild در کامپوننت والد، پارامتر Required مقدار دهی نشده‌است. در این حالت، مقدار پیش‌فرض درج شده‌ی در کامپوننت فرزند، مورد استفاده قرار می‌گیرد؛ وگرنه مقادیر تنظیم شده‌ی توسط کامپوننت والد، حق تقدم بالاتری نسبت به مقادیر پیش‌فرض خواهند داشت.

مشکل! کامپوننت AttributeSplattingChild که فقط به همراه یک المان است، تا این لحظه نیاز به تعریف سه پارامتر جدید را جهت تامین ویژگی‌های آن المان داشته‌است. اگر تعداد این المان‌ها افزایش پیدا کرد، آیا راه بهتری برای مدیریت تعداد بالای ویژگی‌های مورد نیاز وجود دارد؟
پاسخ: در یک چنین حالتی می‌توان ویژگی‌های هر المان را توسط پارامتری از نوع Dictionary مدیریت کرد؛ بجای تعریف تک تک آن‌ها به صورت خواصی مجزا. به این قابلیت، Attribute Splatting می‌گویند.
در این حالت تمام کدهای AttributeSplattingChild.razor به صورت زیر خلاصه می‌شوند:
<div>
    <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4>

    <input id="roomName" @attributes="InputAttributes" class="form-control" />
</div>

@code {
    [Parameter]
    public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object>
    {
        { "required" , "required"},
        { "placeholder", "Initial Text"},
        { "maxlength", 10}
    };
}
در اینجا با استفاده از دایرکتیو جدید attributes@ می‌توان لیستی از key/value‌های ویژگی‌های یک المان را به صورت یک دیکشنری دریافت کرد و دیگر نیازی نیست تا تک تک آن‌ها را تبدیل به یک پارامتر و خاصیت عمومی مجزا کرد. در این حالت مقادیری که در سمت کامپوننت فرزند تعریف می‌شوند، به عنوان مقادیر اولیه‌ی قابل بازنویسی توسط کامپوننت والد، درنظر گرفته خواهند شد (مانند مثال پارامتر Required که عنوان شد).
و همچنین در ادامه کامپوننت والد یا AttributeSplatting.razor نیز به صورت زیر تغییر می‌کند:
@page "/AttributeSplatting"

<h1>Attribute Splatting</h1>

<AttributeSplattingChild InputAttributes="InputAttributesFromParent"></AttributeSplattingChild>

@code{
    Dictionary<string, object> InputAttributesFromParent = new Dictionary<string, object>
    {
        { "required" , "required"},
        { "placeholder", "Enter the Room Name From Parent"},
        { "maxlength", 5}
    };
}
با توجه به اینکه پارامتر InputAttributes، یک شیء دیکشنری را دریافت می‌کند، فیلد آن‌را در قسمت کدهای کامپوننت جاری تعریف کرده و مورد استفاده قرار می‌دهیم. در این حالت هر مقداری که در سمت والد تنظیم شود، حق تقدم بیشتری نسبت به مقدار پیش‌فرض ویژگی‌های تنظیم شده‌ی در کامپوننت فرزند خواهد داشت.



ساده سازی روش تعریف key/value‌های شیء دیکشنری Attribute Splatting

تا اینجا موفق شدیم تعداد قابل ملاحظه‌ای از پارامترهای عمومی یک کامپوننت را تنها توسط یک شیء Dictionary مدیریت کنیم. همچنین همانطور که ملاحظه می‌کنید، هم Dictionary سمت کامپوننت فرزند و هم سمت کامپوننت والد، نیاز به مقدار دهی اولیه‌ای را دارند. این مقدار دهی اولیه را می‌توان به نحو دیگری نیز در حین استفاده‌ی از قابلیت Attribute Splatting، انجام داد:
<div>
    <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4>

    <input id="roomName" @attributes="InputAttributes" placeholder="Initial Text" class="form-control" />
</div>

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object>();
}
در اینجا مقادیر اولیه‌ی دیکشنری تعریف شده را حذف کرده‌ایم و بجای آن‌ها، این مقادیر اولیه را به صورت ویژگی‌های متداول یک المان HTML ای تعریف کرده‌ایم؛ مانند placeholder تعریف شده. برای اینکه یک چنین ویژگی‌هایی به عنوان key/valueهای دیکشنری تعریف شده قابل استفاده باشند، تنها کافی است خاصیت CaptureUnmatchedValues ویژگی پارامتر را به true تنظیم کرد. در اینجا Unmatched Values، همان ویژگی‌هایی هستند که در حین تعریف یک المان اضافه شده‌اند (مانند placeholder در مثال فوق) اما در حین مقدار دهی اولیه‌ی دیکشنری، تعریف نشده‌اند و یا تمام پارامترهای عمومی دیگری که در اینجا ذکر و تعریف نشده‌اند. بنابراین تنها یک CaptureUnmatchedValues = true را در سطح یک کامپوننت می‌توان تعریف کرد.

پس از این تغییر، کامپوننت والد هم به صورت زیر خلاصه می‌شود و دیگر نیازی به تعریف و مقدار دهی InputAttributes و یا تعریف مجزای یک دیکشنری را ندارد. در اینجا هر ویژگی که به المان نسبت داده شود، به عنوان Unmatched Values تفسیر شده و مورد استفاده قرار می‌گیرد.
@page "/AttributeSplatting"

<h1>Attribute Splatting</h1>

<AttributeSplattingChild placeholder="Placeholder default"></AttributeSplattingChild>


اگر به تصویر فوق دقت کنید، هرچند در کامپوننت والد مقدار placeholder، به متن دیگری تنظیم شده، اما متن تنظیم شده‌ی در کامپوننت فرزند، تقدم بیشتری پیدا کرده و نمایش داده شده‌است. علت اینجا است که محل قرارگیری آن در مثال فوق، در سمت راست دایرکتیو attributes@ است. اگر آن‌را در سمت چپ attributes@ قرار دهیم، حق تقدم attributes@ بیشتر شده و مقدار تنظیم شده‌ی در سمت کامپوننت والد، بجای placeholder اولیه‌ی تعریف شده‌ی در اینجا مورد استفاده قرار می‌گیرد:
<input id="roomName" placeholder="Initial Text" @attributes="InputAttributes" class="form-control" />


روش انتقال پارامترها به چندین زیر سطح

در قسمت قبل، ParentComponent.razor و ChildComponent.razor را تعریف و تکمیل کردیم. هدف از آ‌ن‌ها، بررسی ویژگی Render Fragment‌ها بود. در ادامه‌ی آن، یک زیر کامپوننت دیگر را نیز به نام Pages\LearnBlazor\LearnBlazor‍Components\GrandChildComponent.razor اضافه می‌کنیم. هدف این است که کامپوننت Parent، کامپوننت Child را فراخوانی کند و کامپوننت Child، کامپوننت GrandChild را تا یک سلسله مراتب از کامپوننت‌ها را تشکیل دهیم.
محتوای GrandChildComponent را هم بسیار ساده نگه می‌داریم، تا پارامتری رشته‌ای را دریافت کرده و نمایش دهد:
<div class="row">
    <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4>
    <br />
    <p> There is a message - @MessageForGrandChild </p>
</div>

@code {
    [Parameter]
    public string MessageForGrandChild { get; set; }
}
در ChildComponent، کامپوننت GrandChild را به نحو زیر فراخوانی کرده و پارامتری را به آن ارسال می‌کنیم:
<div class="mt-2">
    <GrandChildComponent MessageForGrandChild="@MessageForGrandChild"></GrandChildComponent>
</div>


@code {
    [Parameter]
    public string MessageForGrandChild { get; set; }

   // ...
}
و اکنون در بالاترین سطح این سلسه مراتبی که مشاهده می‌کنید یعنی کامپوننت Parent، این پیام MessageForGrandChild را مقدار دهی خواهیم کرد تا توسط GrandChildComponent نمایش داده شود:
<ChildComponent
    MessageForGrandChild="This is a message from Grand Parent"
    Title="This is the second child component">
    <p><b>@MessageText</b></p>
</ChildComponent>
همانطور که مشاهده می‌کنید، انتقال متداول یک پارامتر، از بالاترین سطح سلسه مراتب کامپوننت‌ها به پایین‌ترین سطح موجود، نیاز به مقدار قابل ملاحظه‌ای کد تکراری را دارد. همچنین برای نمونه پارامتر انتقالی تعریف شده‌ی در کامپوننت Child، اصلا در آن کامپوننت استفاده نمی‌شود و هدف از آن، متصل کردن یک سطح بالاتر، به یک سطح پایین‌تر است.
بنابراین اکنون این سؤال مطرح می‌شود که آیا می‌توان پارامتری را در همان کامپوننت Parent تعریف کرد که توسط کامپوننت GrandChild قابل شناسایی و استفاده باشد، بدون اینکه کامپوننت Child را در این بین تغییر دهیم؟
پاسخ: بله. برای اینکار ویژگی‌های CascadingValue و CascadingParameter در Blazor پیش بینی شده‌اند.
در ابتدا، پارامتر MessageForGrandChild کامپوننت Child حذف کرده و سپس آن‌را توسط کامپوننت توکار CascadingValue محصور می‌کنیم. در اینجا نیاز است مقدار انتقالی را نیز مشخص کنیم:
<CascadingValue Value="@MessageForGrandChild">
    <ChildComponent        
        Title="This is the second child component">
        <p><b>@MessageText</b></p>
    </ChildComponent>
</CascadingValue>

@code {
    string MessageForGrandChild = "This is a message from Grand Parent";
پس از این تعریف، به کامپوننت Child مراجعه کرده و پارامتر MessageForGrandChild آن‌را حذف می‌کنیم؛ چون دیگر نیازی به آن نیست. همچنین در این کامپوننت، فراخوانی GrandChildComponent نیز به صورت زیر خلاصه شده و دیگر نیازی به ذکر پارامتر انتقالی MessageForGrandChild حذف شده را ندارد:
<GrandChildComponent></GrandChildComponent>
در آخر به کامپوننت GrandChild مراجعه کرده و اینبار پارامتر مورد استفاده‌ی در آن‌را با ویژگی جدید CascadingParameter مزین می‌کنیم:
[CascadingParameter]
public string MessageForGrandChild { get; set; }


چند نکته:
- در اینجا نوع CascadingParameter تعریف شده، باید با نوع Value کامپوننت CascadingValue، در بالاترین سطح سلسله مراتب کامپوننت‌ها، یکی باشد.
- نام CascadingParameter تعریف شده مهم نیست. فقط نوع آن مهم است.
- تمام کامپوننت‌های موجود و پوشش داده شده‌ی در سلسله مراتب جاری، قابلیت تعریف CascadingParameter ای مانند مثال فوق را دارند و این تعریف، محدود به پایین‌ترین سطح موجود نیست. برای مثال در اینجا در کامپوننت Child هم در صورت نیاز می‌توان همین CascadingParameter را تعریف و استفاده کرد.


روش تعریف پارامترهای آبشاری نام‌دار

تا اینجا روش انتقال یک پارامتر را از بالاترین سطح، به پایین‌ترین سطح سلسله مراتب کامپوننت‌های تعریف شده، بررسی کردیم. اکنون شاید این سؤال مطرح شود که اگر خواستیم بیش از یک پارامتر را بین اجزای این سلسله، به اشتراک بگذاریم چه باید کرد؟
در این حالت می‌توان پارامتر جدید را توسط یک کامپوننت CascadingValue تو در تو، به صورت زیر معرفی کرد؛ که اینبار نامدار نیز هست:
<CascadingValue Value="@MessageForGrandChild" Name="MessageFromGrandParent">
    <CascadingValue Value="@Number" Name="GrandParentsNumber">
        <ChildComponent
            Title="This is the second child component">
            <p><b>@MessageText</b></p>
        </ChildComponent>
    </CascadingValue>
</CascadingValue>

@code {
    string MessageForGrandChild = "This is a message from Grand Parent";
    int Number = 7;
برای نمونه در این مثال، عدد 7 نیز قرار است در اختیار سلسله مراتب شروع شده‌ی از کامپوننت جاری، قرار گیرد. به همین جهت یک CascadingValue تو در توی مختص آن نیز تعریف شده‌است که اینبار نامش GrandParentsNumber است.

پس از این تغییر، GrandChildComponent، این پارامترهای نامدار را از طریق ذکر صریح خاصیت Name ویژگی CascadingParameter، دریافت می‌کند:
<div class="row">
    <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4>
    <br />
    There is a message: @Message
    <br />
    GrandParentsNumber: @Number
</div>

@code {
    [CascadingParameter(Name = "MessageFromGrandParent")]
    public string Message { get; set; }

    [CascadingParameter(Name = "GrandParentsNumber")]
    public int Number { get; set; }
}


یک نکته: چون نوع پارامترهای ارسالی یکی نیست، الزامی به ذکر نام آن‌ها نبود. در این حالت بر اساس نوع پارامترهای آبشاری، عملیات اتصال مقادیر صورت می‌گیرد. اما اگر نوع هر دو را برای مثال رشته‌ای تعریف می‌کردیم، مقدار Number، بر روی مقدار MessageForGrandChild بازنویسی می‌شد. یعنی در UI، هر دو پارامتر هم نوع، یک مقدار را نمایش می‌دادند که در حقیقت مقدار پایین‌ترین CascadingValue تعریف شده‌است. بنابراین ذکر نام پارامترهای آبشاری، روشی‌است جهت تمایز قائل شدن بین پارامترهای هم نوع.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-09.zip
مطالب
فیلترها در MVC
هنگامی که درخواستی به سرور ارسال می‌گردد، به کنترلر و اکشن مربوطه جهت پاسخگویی هدایت می‌شود. خب شاید مواقعی شما نیاز داشته باشید قبل یا بعد از اجرای اکشن متدی، کدی اجرا گردد. به‌همین جهت در MVC قابلیتی بنام Filter ارائه گردید.
فیلتر، یک کلاس سفارشی است که شما می‌توانید منطق برنامه را جهت اجرا، قبل یا بعد از اجرای یک اکشن متد، در آن پیاده سازی نمایید. فیلترها می‌توانند به یک اکشن متد و یا کنترلری منتسب شوند که در ادامه با این روشها آشنا خواهید شد.

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

 نوع توضیح
 فیلتر توکار
 اینترفیس
 Authorization
انجام عملیات احراز هویت و سطح دسترسی، قبل از اجرای کد اکشن متد  
 [Authorize] و [RequireHttps]  
 IAuthorizationFilter 
 Action
اجرای کدهایی قبل از اجرای کدهای اکشن متد 
   IActionFilter 
 Result
اجرای کدهایی قبل یا بعد از تولید ویو (View result) 
 [OutputCache]   IResultFilter 
Exception
اجرای کدهایی در صورت وجود استثنای مدیریت نشده 
[HandleError] 
IExceptionFilter
مثال: هنگامی که خطایی در حین اجرای اکشن متدی رخ می‌دهد، فیلتر توکار MVC بنام HandleError اجرا می‌شود. این فیلتر توکار فایل Error.cshtml را که در فولدر Shared قرار دارد، رندر می‌کند و نمایش می‌دهد.
در تکه کد زیر نحوه‌ی استفاده از این فیلتر را مشاهده می‌کنید:
[HandleError]
public class HomeController : Controller
{
    public ActionResult Index()
    {
        //throw exception for demo
        throw new Exception("This is unhandled exception");
            
        return View();
    }

    public ActionResult About()
    {
        return View();
    }

    public ActionResult Contact()
    {
        return View();
    }        
}

نکته: فیلترهای اعمال شده‌ی به یک کنترلر، به تمام اکشن متدهای آن نیز اعمال می‌گردند. 

در کد بالا خصیصه‌ی HandleError به HomeController اعمال شده است. بنابراین در صورت بروز خطایی در هر کدام از اکشن‌ها، صفحه‌ی Error.cshtml نمایش داده خواهد شد و در تظر داشته باشید که خطاها توسط try-catch هندل نشده‌اند.
باید جهت عملکرد صحیح فیلتر توکار HandleErrorAttribute، مقدار customErrors در قسمت System.web فایل web.config مساوی on باشد.
<customErrors mode="On" />


مهیا کنندگان فیلترها

بصورت پیش فرض MVC از سه طریق زیر فیلترها را جهت استفاده‌ی در برنامه فراهم می‌کند:

  1. خصیصه‌ی GlobalFilters.Filters برای فیلترهای سراسری
  2. کلاس FilterAttributeFilterProvider برای فیلترهای خصیصه‌ای
  3. کلاس ControllerInstanceFilterProvider جهت افزودن کنترلر به یک وهله از FilterProviderCollection

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

ترتیب اجرای فیلترها

همانطور که در ابتدا اشاره شد، در MVC چهار نوع فیلتر معرفی شده است که امکان استفاده‌ی از آنها به‌صورت همزمان در سطح کنترلر و یا اکشن متد وجود دارد. اما ترتیب  اجرای آنها متفاوت و به ترتیب زیر است:

  1. فیلترهای Authorization
  2. فیلترهای Action
  3. فیلترهای Result یا Response
  4. فیلترهای Exception

فیلترها براساس ترتیب اشاره شده‌ی در بالا اجرا خواهند شد. در صورتیکه چند فیلتر از یک نوع استفاده شود، جهت تقدم و تاخر در اجرا، از خاصیت Order استفاده خواهد شد. بعنوان مثال در کد زیر بدلیل خاصیت Order=1 ابتدا AuthorizationFilterB  و سپس AuthorizationFilterA اجرا می‌شود.

[AuthorizationFilterA(Order=2)]
[AuthorizationFilterB(Order=1)]
public ActionResult Index()
{          
    return View();
}
علاوه بر خاصیت Order، مقدار Scope نیز سطح سومی از ترتیب اجرای فیلترها می‌باشد. مقادیر Scope بشرح زیر است:
public enum FilterScope
{
    First = 0,
    Global = 10,
    Controller = 20,
    Action = 30,
    Last = 100,
}
این خصیصه‌ی فیلترها در محل بکار گیری آنها مقدار دهی می‌شود. در صورتیکه فیلتری بصورت سراسری رجیستر شود، Scope آن برابر 10 و در سطح کنترلر، برابر 20 خواهد بود و الی آخر.

نکته: مقدار Scope فیلترهای Authorization برابر 0 و فیلترهای Exception برابر 100 می‌باشد.

ایجاد فیلتر سفارشی

روش اول: پیاده سازی اینترفیس یکی از انواع فیلترها و ارث بری از کلاس FilterAttribute

در این روش متدهایی که باید پیاده سازی شوند متفاوت خواهد بود. به همین جهت متدهای هر نوع بشرح زیر معرفی می‌شود:

  • IAuthorizationFilter
// Called when authorization is required
void OnAuthorization(AuthorizationContext filterContext)
  • IActionFilter
// Called after the action method executes
void OnActionExecuted(ActionExecutedContext filterContext)

// Called before an action method executes
void OnActionExecuting(ActionExecutingContext filterContext)
  • IResultFilter
// Called after an action result executes
void OnResultExecuted(ResultExecutedContext filterContext)

// Called before an action result executes
void OnResultExecuting(ResultExecutedContext filterContext)
  • IException
// Called when an exception occurs
void OnException(ExceptionContext filterContext)

یادآوری: همانطور که در ابتدای مقاله اشاره شد، فیلترها قبل یا بعد از اجرای اکشن متدها فراخوانی خواهند شد. بنابراین به کامنت بالای متد فیلترها دقت داشته باشید.

مثال: پیاده سازی اینترفیس IExceptionFilter و ارث بری از کلاس FilterAttribute جهت تهیه‌ی فیلتری سفارشی از نوع Exception

class CustomErrorHandler : FilterAttribute, IExceptionFilter
{
    public override void IExceptionFilter.OnException(ExceptionContext filterContext)
    {
        Log(filterContext.Exception);

        base.OnException(filterContext);
    }

    private void Log(Exception exception)
    {
        //log exception here..
    }
}

روش دوم:
ارث بری از ActionFilterAttribute
کلاس abstract فوق دارای چهار متد زیر جهت تحریف است. همانطور که مشاهده می‌کنید این کلاس علاوه بر دو متد OnActionExecuted و OnActionExecuting دارای دو متد دیگر OnResultExecuting و OnResultExecuted که به‌ترتیب قبل و بعد خروجی (Result) اکشن متد اجرا می‌شوند، نیز می‌باشد. این نوع فیلترها عموما جنبه‌ی استفاده عمومی داشته و می‌توان از آنها جهت logging ،caching و یا authorization استفاده کرد.
// Called by MVC after the action method executes
void OnActionExecuted(ActionExecutedContext filterContext)

// Called by MVC before the action method executes
void OnActionExecuted(ActionExecutedContext filterContext)

// Called by MVC after the action result executes
void OnResultExecuted(ResultExecutedContext filterContext)

// Called by MVC before the action result executes
void OnResultExecuting(ResultExecutingContext filterContext)

مثال: کلاس LogAttribute که از کلاس ActionFilterAttribute ارث بری کرده است، عملیات قبل و بعد از اجرای اکشن متد را لاگ می‌کند.
public class LogAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        Log("OnActionExecuted", filterContext.RouteData); 
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        Log("OnActionExecuting", filterContext.RouteData);      
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        Log("OnResultExecuted", filterContext.RouteData);      
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
        Log("OnResultExecuting ", filterContext.RouteData);      
    }

    private void Log(string methodName, RouteData routeData)
    {
        var controllerName = routeData.Values["controller"];
        var actionName = routeData.Values["action"];
        var message = String.Format("{0}- controller:{1} action:{2}", methodName, 
                                                                    controllerName, 
                                                                    actionName);
        Debug.WriteLine(message);
    }
}

روش سوم:
پیاده سازی داخل کنترلر
کلاس Controller  می‌تواند هر یک از اینترفیس‌های فیلترها را پیاده سازی نماید. به عبارت دیگر در هر کلاس کنترلر می‌توانید متدهای زیر را تحریف نمایید.
  • OnAuthorization ^
  • OnException ^
  • OnActionExecuting ^
  • OnActionExecuted ^
  • OnResultExecuting ^
  • OnResultExecuted ^


روش چهارم: ارث بری از کلاس فیلترهای توکار و مهیای در MVC و تحریف متدهای آن 
در کد زیر با تحریف و سفارشی سازی متد OnException مخصوص فیلتر توکار HandleError، قابلیت‌های آن افزایش یافته است:

class CustomErrorHandler : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        Log(filterContext.Exception);

        base.OnException(filterContext);
    }

    private void Log(Exception exception)
    {
        //log exception here..
    }
}


رجیستر فیلترها

  • سراسری:

درصورتی که قصد داشته باشید فیلتری بصورت سراسری و در کل برنامه فعال گردد باید آن را در رویداد Application_Start فایل Global.asax.cs بوسیله‌ی متد RegisterGlobalFilters کلاس FiterConfig رجیستر نمایید. بعد از آن فیلتر به کلیه‌ی کنترلرها و اکشن متدها اعمال می‌گردد.

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
          FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    }
}

// FilterConfig.cs located in App_Start folder 
public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());

        // add your new custom filters
        filters.Add(new LogAttribute()); 
        filters.Add(new CustomErrorHandler());
     }
}

در کد بالا فیلتر توکار HandleError و البته فیلترهای سفارشی دیگری نیز به صورت سراسری به تمام اکشن متدهای کنترلرها اعمال گردیده است.

  • کنترلر: در صورتی که فقط بخواهید یک فیلتر به کل اکشن‌های یک کنترلر اعمال گردد. همانند آنچه که در مثال ابتدایی بدان اشاره شد.
[HandleError]
public class HomeController : Controller
  • اکشن متد: اعمال یک فیلتر به یک اکشن متد خاص کنترلر. در کد زیر فیلتر HandleError فقط به اکشن متد Index کنترلر Home اعمال خواهد شد.
public class HomeController : Controller
{
    [HandleError]
    public ActionResult Index()
    {
        return View();
    }
}