مطالب
Blazor 5x - قسمت هشتم - مبانی Blazor - بخش 5 - تامین محتوای نمایشی کامپوننت‌های فرزند توسط کامپوننت والد
تا اینجا با نحوه‌ی تعریف کامپوننت‌ها و انتقال اطلاعات به آن‌ها و برعکس، آشنا شدیم. در ادامه علاقمندیم اگر کامپوننتی را به صورت زیر تعریف کردیم:
<Comp1>This is a content coming from the parent</Comp1>
محتوایی را که بین تگ‌های این کامپوننت فرزند، توسط کامپوننت والد تنظیم شده‌است، در قسمت مشخصی از UI کامپوننت فرزند نمایش دهیم.


معرفی مفهوم Render Fragment

برای درج محتوای تامین شده‌ی توسط کامپوننت والد در یک کامپوننت فرزند، از ویژگی به نام Render Fragment استفاده می‌شود. مثالی جهت توضیح جزئیات آن:
در ابتدا یک کامپوننت والد جدید را در مسیر Pages\LearnBlazor\ParentComponent.razor به صورت زیر تعریف می‌کنیم:
@page "/ParentComponent"

<h1 class="text-danger">Parent Child Component</h1>

<ChildComponent Title="This title is passed as a parameter from the Parent Component">
    A `Render Fragment` from the parent!
</ChildComponent>

<ChildComponent Title="This is the second child component"></ChildComponent>

@code {

}
- در اینجا یک کامپوننت متداول با مسیریابی منتهی به ParentComponent/ تعریف شده‌است.
- سپس دوبار کامپوننت فرضی ChildComponent به همراه پارامتر Title و یک محتوای جدید قرار گرفته‌ی در بین تگ‌های آن، در صفحه تعریف شده‌اند.
- بار دومی که ChildComponent در صفحه قرار گرفته‌است، به همراه محتوای جدیدی در بین تگ‌های خود نیست.

برای دسترسی به این کامپوننت از طریق منوی برنامه، مدخل منوی آن‌را به کامپوننت Shared\NavMenu.razor اضافه می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="ParentComponent">
       <span class="oi oi-list-rich" aria-hidden="true"></span> Parent/Child Relation
    </NavLink>
</li>
اکنون می‌خواهیم کامپوننت ChildComponent را افزوده و انتظارت یاد شده (دریافت یک پارامتر و نمایش محتوای سفارشی تنظیم شده) را پیاده سازی کنیم. به همین جهت فایل جدید Pages\LearnBlazor\LearnBlazor‍Components\ChildComponent.razor را با محتوای زیر ایجاد می‌کنیم:
<div>
    <div class="alert alert-info">@Title</div>
    <div class="alert alert-success">
        @if (ChildContent == null)
        {
            <span> Hello, from Empty Render Fragment </span>
        }
        else
        {
            <span>@ChildContent</span>
        }
    </div>
</div>


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

    [Parameter]
    public RenderFragment ChildContent { get; set; }
}
توضیحات:

- خاصیت عمومی Title که توسط ویژگی Parameter مزین شده‌است، امکان تنظیم مقدار مشخصی را توسط کامپوننت دربرگیرنده‌ی ChildComponent میسر می‌کند.
- در اینجا پارامتر عمومی دیگری نیز تعریف شده‌است که اینبار از نوع ویژه‌ی RenderFragment است. توسط آن می‌توان به محتوایی که در کامپوننت والد ChildComponent در بین تگ‌های آن تنظیم شده‌است، دسترسی یافت. همچنین اگر این محتوا توسط کامپوننت والد تنظیم نشده باشد، مانند دومین باری که ChildComponent در صفحه قرار گرفته‌است، می‌توان با بررسی نال بودن آن، یک محتوای پیش‌فرض را نمایش داد.


با این خروجی:



روش دیگری برای فراخوانی Event Call Back ها

در قسمت قبل روش انتقال اطلاعات را از کامپوننت‌های فرزند، به والد مشاهده کردیم. فراخوانی آن‌ها در سمت Child Component نیاز به یک متد اضافی داشت و همچنین تنها یک پارامتر را هم ارسال کردیم. برای ساده سازی این عملیات از روش زیر نیز می‌توان استفاده کرد:
<button class="btn btn-danger"
        @onclick="@(() => OnClickBtnMethod.InvokeAsync((1, "A message from child!")))">
   Show a message from the child!
</button>


@code {
    // ...

    [Parameter]
    public EventCallback<(int, string)> OnClickBtnMethod { get; set; }
}
- در اینجا برای تعریف و ارسال بیش از یک پارامتر، از tuples استفاده شده‌است.
- همچنین فراخوانی OnClickBtnMethod.InvokeAsync را نیز در محل تعریف onclick@ بدون نیازی به یک متد اضافی، مشاهده می‌کنید. نکته‌ی مهم آن، قرار دادن این قطعه کد داخل ()@ است تا ابتدا و انتهای کدهای #C مشخص شود؛ وگرنه کامپایل نمی‌شود.

در سمت کامپوننت والد برای دسترسی به OnClickBtnMethod که اینبار یک tuple را ارسال می‌کند، می‌توان به صورت زیر عمل کرد:
@page "/ParentComponent"

<h1 class="text-danger">Parent Child Component</h1>

<ChildComponent
    OnClickBtnMethod="ShowMessage"
    Title="This title is passed as a parameter from the Parent Component">
    A `Render Fragment` from the parent!
</ChildComponent>

<ChildComponent Title="This is the second child component">
    <p><b>@MessageText</b></p>
</ChildComponent>

@code {
    string MessageText = "";

    private void ShowMessage((int Value, string Message) args)
    {
        MessageText = args.Message;
    }
}
در اینجا OnClickBtnMethod تعریف شده، به متد ShowMessage متصل شده‌است که یک tuple از جنس (int, string) را دریافت می‌کند. سپس با انتساب خاصیت رشته‌ای آن به فیلد جدید MessageText، سبب نمایش آن می‌شویم.


امکان تعریف چندین RenderFragment

تا اینجا یک RenderFragment را در کامپوننت فرزند تعریف کردیم. امکان تعریف چندین RenderFragment در ChildComponent.razor نیز وجود دارند:
@code {
    // ...

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    [Parameter]
    public RenderFragment DangerChildContent { get; set; }
}
در یک چنین حالتی، روش استفاده‌ی از این پارامترهای RenderFragment در سمت والد (ParentComponent.razor) به صورت زیر خواهد بود:
<ChildComponent
    OnClickBtnMethod="ShowMessage"
    Title="This title is passed as a parameter from the Parent Component">
    <ChildContent>
        A `Render Fragment` from the parent!
    </ChildContent>
    <DangerChildContent>
        A danger content from the parent!
    </DangerChildContent>
</ChildComponent>
که به همراه ذکر صریح نام پارامترها به صورت تگ‌هایی جدید، داخل تگ اصلی است.

از آنجائیکه ذکر این تگ‌ها اختیاری است، نیاز است در ChildComponent.razor بر اساس null بودن آن‌ها، تصمیم به رندر محتوایی پیش‌فرض گرفت:
    @if(DangerChildContent == null)
    {
        @if (ChildContent == null)
        {
            <span> Hello, from Empty Render Fragment </span>
        }
        else
        {
            <span>@ChildContent</span>
        }
    }
    else
    {
        <span>@DangerChildContent</span>
    }




کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-08.zip
نظرات مطالب
فعال سازی قسمت ارسال فایل و تصویر ویرایشگر آنلاین RedActor در ASP.NET MVC
یک نکته تکمیلی رو اینجا اضافه کنم.
نحوه عملکرد RedActor در نگارش جدید آن با نگارش قبلی که من استفاده کردم، کاملا تفاوت کرده و قسمت‌هایی از مطلب فوق دیگر معتبر نیست.
RedActor اینبار به خروجی json نیاز دارد. برای مثال به فایل مثال image_upload.php آن دقت کنید. در نگارش جدید به این نوع خروجی نیاز دارد:
    // displaying file    
$array = array(
'filelink' => '/images/'.$filename
);

echo stripslashes(json_encode($array));   
معادل آن در ASP.NET MVC به نحو زیر است:
var array = new { filelink = "Images/k1.jpg" };
return Json(array, System.Net.Mime.MediaTypeNames.Text.Plain, JsonRequestBehavior.AllowGet);
یک anonymously typed object دلخواه را مطابق array مثال PHP آن درست کنید و به Json result به نحو فوق ارسال کنید.
برای مابقی هم به همین ترتیب است. مثلا file_upload.php مثال آن اینبار به شکل زیر تعریف شده:
$array = array(
'filelink' => '/files/'.$_FILES['file']['name'],
'filename' => $_FILES['file']['name']
);

echo stripslashes(json_encode($array));
معادل ASP.NET MVC آن به نحو زیر است:
var array = new { filelink = "files/myfile.zip", filename="myfile.zip" };
return Json(array, System.Net.Mime.MediaTypeNames.Text.Plain, JsonRequestBehavior.AllowGet);

مطالب دوره‌ها
استفاده از Adobe iFilter برای جستجوی Full Text در فایل‌های PDF
در قسمت‌های قبل، نحوه‌ی کار با فیلترهای FTS آفیس را بررسی کردیم. شرکت Adobe نیز برای جستجوی Full-Text بر روی فایل‌های PDF، یک iFilter خاص را طراحی کرده‌است که نسخه‌ی آخر آن‌را از آدرس ذیل می‌توانید دریافت کنید:
یک تجربه‌ی مهم: نگارش 11 آن را با SQL Server X64 تست کردم کار نکرد. اما نگارش 9 کار می‌کند.
مستندات کامل


پس از نصب ابتدایی آن، مراحل ذیل را برای فعال سازی آن باید طی کرد:

1) تنظیم مسیر پوشه bin نصب فیلتر (مهم!)
 Start > Control Panel > System > Advanced
Environment Variables -> System Variables -> find PATH
مسیر فوق را در تنظیمات ویندوز یافته و سپس به انتهای Path، آدرس پوشه bin فیلتر نصب شده را اضافه کنید:
 C:\Program Files\Adobe\Adobe PDF iFilter 9 for 64-bit platforms\bin\
دقت داشته باشید که این مسیر باید به \ ختم شود.
سپس کل سیستم را ری استارت کنید.


2) ثبت آن در وهله‌ی جاری SQL Server
برای این منظور ابتدا دستورات ذیل را جرا کنید:
 exec sys.sp_fulltext_service 'load_os_resources', 1;
EXEC sp_fulltext_service 'verify_signature', 0
EXEC sp_fulltext_service 'update_languages'; -- update language list
EXEC sp_fulltext_service 'restart_all_fdhosts'; -- restart daemon
reconfigure with override
گزینه‌ی verify_signature مربوط به فایل‌های iFilter ایی است که امضای دیجیتال ندارند.
سپس در management studio یکبار بر روی وهله‌ی جاری کلیک راست کرده و گزینه‌ی Restart را انتخاب کنید (مهم).

3) پس از ری‌استارت SQL Server، اطمینان حاصل کنید که این فیلتر جدید نصب شده‌است:
 exec sys.sp_help_fulltext_system_components 'filter';


و یا کوئری ذیل نیز برای این منظور مفید است:
 SELECT document_type, path from sys.fulltext_document_types where document_type = '.pdf'

4) در ادامه برای استفاده از آن آزمایش ذیل را ترتیب خواهیم داد.

ایجاد یک جدول جدید که فایل‌های باینری PDF را در خود ذخیره می‌کند:
 CREATE TABLE PdfDocuments
(
id INT IDENTITY(1,1) NOT NULL,
doctype NCHAR(4) NOT NULL,
doccontent VARBINARY(MAX) NOT NULL,
CONSTRAINT PK_PdfDocuments
PRIMARY KEY CLUSTERED(id)
);
ستون‌های doctype معرف نوع سند و doccontent ذخیره کننده‌ی محتوای کامل فایل‌های PDF خواهند بود.
سپس چند رکورد را در آن ثبت می‌کنیم. برای نمونه دو مقاله‌ی خروجی PDF سایت جاری را در این جدول ثبت خواهیم کرد:
 INSERT INTO PdfDocuments(doctype, doccontent)
SELECT
N'PDF',
bulkcolumn FROM OPENROWSET (BULK 'C:\Users\Vahid\Downloads\1732-DotNetTips.pdf', SINGLE_BLOB) AS doc;

INSERT INTO PdfDocuments(doctype, doccontent)
SELECT
N'PDF',
bulkcolumn FROM OPENROWSET (BULK 'C:\Users\Vahid\Downloads\1733-DotNetTips.pdf', SINGLE_BLOB) AS doc;
در ادامه علاقمندیم تا بر روی خواص و متادیتای فایل‌های PDF نیز بتوانیم جستجوی FTS انجام دهیم. به همین منظور search propery list متناظری را نیز تعریف خواهیم کرد. همانطور که در قسمت‌های قبل عنوان شد، نیاز است GUID هر خاصیت را برای تعریف از سازنده‌ی iFilter دریافت کرد. این اطلاعات در سند ذیل مستند شده‌اند:

 -- Search property list
CREATE SEARCH PROPERTY LIST PdfSearchPropertyList;
GO
ALTER SEARCH PROPERTY LIST PdfSearchPropertyList
ADD 'Author'
WITH (PROPERTY_SET_GUID = 'F29F85E0-4FF9-1068-AB91-08002B27B3D9',
PROPERTY_INT_ID = 4,
PROPERTY_DESCRIPTION = 'Author - author of a given item.');
GO

در اینجا اگر علاقمند بودید، stop list معرفی شده در قسمت‌های قبل را نیز می‌توان افزود.
 CREATE FULLTEXT STOPLIST SQLStopList;
GO
-- Add a stopwords
ALTER FULLTEXT STOPLIST SQLStopList ADD N'به' LANGUAGE 'English';
ALTER FULLTEXT STOPLIST SQLStopList ADD N'با' LANGUAGE 'English';
--.....

سپس یک کاتالوگ FTS و ایندکس Full-Text ایی را بر روی این جدول ایجاد می‌کنیم:
 -- Full-text catalog
CREATE FULLTEXT CATALOG PdfDocumentsFtCatalog;
GO

-- Full-text index
CREATE FULLTEXT INDEX ON PdfDocuments
(
  [doccontent] TYPE COLUMN [doctype]
  Language 1033
  STATISTICAL_SEMANTICS
)
KEY INDEX PK_PdfDocuments
ON PdfDocumentsFtCatalog
WITH STOPLIST = SQLStopList,
  SEARCH PROPERTY LIST = PdfSearchPropertyList,
  CHANGE_TRACKING AUTO;
GO

آیا کار می‌کند ؟ چیزی ایندکس شده‌است؟
 SELECT
 I.document_id,
 I.display_term,
 I.occurrence_count
FROM sys.dm_fts_index_keywords_by_document(DB_ID(DB_NAME()), OBJECT_ID(N'dbo.PdfDocuments')) AS I
INNER JOIN dbo.PdfDocuments D
ON D.id = I.document_id;


انجام دو کوئری بر روی آن. یکی برای یافتن متنی ساده و دیگری برای یافتن خواص

 SELECT *
FROM PdfDocuments
WHERE CONTAINS(doccontent, N'است')

SELECT
 I.document_id,
 I.display_term,
 I.property_id
FROM sys.dm_fts_index_keywords_by_property(DB_ID(DB_NAME()), OBJECT_ID(N'dbo.PdfDocuments')) AS I
INNER JOIN dbo.PdfDocuments D
ON D.id = I.document_id;


 
مطالب
Tag Helper Components در ASP.NET Core 2.0
Tag Helper Components یکی از ویژگی‌های جدید ASP.NET Core 2.0 است و هدف آن میسر ساختن ایجاد و یا ویرایش المان‌های HTML ایی در حال رندر در صفحه هستند. برای مثال یکی از کاربردهای آن‌ها می‌تواند افزودن اسکریپتی به صورت پویا به تمام صفحات سایت باشد؛ مانند روش مایکروسافت برای افزودن Application Insights به برنامه‌های ASP.NET Core. در این حالت متد UserApplicationInsights یک tag helper component را به سیستم تزریق وابستگی‌ها اضافه می‌کند که کار آن افزودن اسکریپت‌های Application Insights به برنامه است؛ بدون اینکه نیازی باشد تا صفحات برنامه را جهت درج این اسکریپت‌ها ویرایش کرد یا تغییر داد.

یک مثال: تهیه‌ی یک TagHelperComponent جهت ویرایش تگ‌های article

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace TestTagHelperComponent2.Utils
{
    public class ArticleTagHelperComponent : TagHelperComponent
    {
        public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase))
            {
                output.PostContent.AppendHtml("<script>console.log('Running ArticleTagHelperComponent');</script>");
            }
            return Task.CompletedTask;
        }
    }
}
در اینجا کار با ارث بری از کلاس پایه TagHelperComponent شروع می‌شود. عملکرد آن این است که اگر موتور Razor به پردازش تگ article رسید:
<article>
    For Testing the TagHelperComponent.
</article>
آنگاه اسکریپتی را که ملاحظه می‌کنید در این بین درج کند.

در ادامه برای اینکه سیستم را از وجود این TagHelperComponent مطلع کنیم، باید آن‌را به صورت یک سرویس جدید، به فایل آغازین برنامه معرفی کنیم:
public void ConfigureServices(IServiceCollection services)
{
   services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>();
   services.AddMvc();
}
این نوع کامپوننت‌ها تمام تگ‌های مشخص article موجود در صفحه را هدف قرار می‌دهند. اما ... اگر آن‌را اجرا کنید اتفاقی خاصی رخ نخواهد داد!
نکته‌ی مهم TagHelperComponentها این است که در قسمت بررسی تگ در حال پردازش:
 if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase))
اگر این تگ ویژه که در اینجا برای مثال article نام دارد، پیشتر تحت عنوان یک TagHelperComponentTagHelper ثبت شده باشد، آنگاه قابلیت اجرا و تحت تاثیر قراردادن این تگ را خواهد یافت. به همین جهت باید این تگ را به عنوان HtmlTargetElement به صورت ذیل تعریف کرد:
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace TestTagHelperComponent2.Utils
{
    [HtmlTargetElement("article")]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class ArticleTagHelperComponentTagHelper : TagHelperComponentTagHelper
    {
        public ArticleTagHelperComponentTagHelper(
            ITagHelperComponentManager componentManager,
            ILoggerFactory loggerFactory)
        : base(componentManager, loggerFactory)
        {
        }
    }
}
سپس آن‌را به فایل Views\_ViewImports.cshtml به نحو زیر اضافه نمود:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, TestTagHelperComponent2
در اینجا TestTagHelperComponent2 نام اسمبلی جاری است که حاوی ArticleTagHelperComponentTagHelper می‌باشد.

پس از این تنظیمات است که اگر برنامه را اجرا کنید، این تغییر را (درج اسکریپت در بین تگ article) ملاحظه خواهید کرد:



Tag Helper Components توکار ASP.NET Core 2.0

در حال حاضر دو TagHelperComponent به نام‌های HeadTagHelper و BodyTagHelper به صورت پیش فرض به سیستم اضافه شده‌اند. یعنی تگ‌های head و body در ASP.NET Core 2.0 را می‌توان توسط TagHelperComponent تحت تاثیر قرار داد و نیازی به تنظیمات TagHelperComponentTagHelper اضافه‌ی فوق برای آن‌ها وجود ندارد.
یک مثال:
    public class MyHeadTagHelperComponent : TagHelperComponent
    {
        public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "head", StringComparison.OrdinalIgnoreCase))
            {
                output.PostContent.AppendHtml("<script>console.log('head tag');</script>");
            }
            return Task.CompletedTask;
        }
    }
در اینجا چون تگ ویژه‌ی head پیشتر در سیستم ثبت شده‌است، مقایسه‌ی انجام شده معتبر بوده و برای فعالسازی آن تنها کاری را که باید انجام داد، ثبت سرویس آن است (البته به شرطی که Microsoft.AspNetCore.Mvc.TagHelpers در فایل Views\_ViewImports.cshtml پیشتر تعریف شده باشد):
public void ConfigureServices(IServiceCollection services)
{
   services.AddTransient<ITagHelperComponent, MyHeadTagHelperComponent>();
   services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>();
   services.AddMvc();
}
اینکار سبب درج اسکریپتی پیش از بسته شدن تگ head صفحه می‌شود:

مطالب
اصول طراحی شیء گرا: OO Design Principles - قسمت پنجم

دانای اطلاعات ( Information Expert )

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

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

public class User
    {
        public ShoppingCart ShoppingCart { get; set; }
        public void AddItem(string name) {
            // User class must know how to create OrderItem
            var item = new OrderItem() { Name = name };

            // User class must know how to add item to shopping cart
            ShoppingCart.Items.Add(item);
            
        }
        public void CheckOut() {
            // User class must know logic behind cost and discount calculations:
            // check for discount
            // check shipping method
            // check promotions
            // calculate total cost of items 
        }
    }
    public class OrderItem
    {
        public int Id { get; set; }
        public string Name { get; set; }

    }
    public class ShoppingCart
    {
        public int Id { get; set; }
        public List<OrderItem> Items { get; set; }
       
    }

بنابراین به جای این طراحی، مسئولیت‌ها را به ShoppingCart منتقل میکنیم:

 public class User
    {
        public ShoppingCart ShoppingCart { get; set; }
        
    }
    public class OrderItem
    {
        public int Id { get; set; }
        public string Name { get; set; }

    }
    public class ShoppingCart
    {
        public int Id { get; set; }
        public List<OrderItem> Items { get; set; }
        public void AddItem(string name)
        {
            // ShoppingCart class know how to create OrderItem
            var item = new OrderItem() { Name = name };

            // ShoppingCart class already know how to add item 
            Items.Add(item);

        }
        public void CheckOut()
        {
            // ShoppingCart class know logic behind cost and discount calculations:
            // check for discount
            // check shipping method
            // check promotions
            // calculate total cost of items 
        }
    }


اتصال ضعیف ( Low Coupling )

با اتصال ضعیف نیز که از ویژگی‌های یک طراحی خوب است آشنا هستیم. هر چه تعداد و نوع اتصال بین مولفه‌ها کم‌تر و ضعیف‌تر باشد، اعمال تغییرات راحت‌تر صورت خواهد گرفت. طراحی با اتصال مناسب سه ویژگی را دارد:

-  وابستگی بین کلاس‌ها کم است.

-  تغییرات در یک کلاس، اثر کمی بر دیگر کلاس‌ها دارد.

-  پتانسیل استفاده‌ی مجدد از مؤلفه‌ها بالا است.

چنانچه قبلا هم اشاره کردم، نوشتن نرم افزاری بدون اتصال، ممکن نیست و باید مؤلفه‌ها با هم همکاری کرده و وظایف را انجام دهند. با این حال میتوان نوع اتصالات و تعداد آنرا بهبود بخشید.


چند ریختی ( Polymorphism )

چند ریختی که از ویژگی‌های اساسی برنامه نویسی و زبان‌های شیء گراست، به منظور بالا بردن قابلیت استفاده‌ی مجدد، استفاده میشود. بر طبق این اصل، مسئولیت تعریف رفتارهای وابسته به نوع کلاس (زیرنوع‌ها در روابط ارث بری) باید به کلاسی واگذار شود که تغییر رفتار در آن اتفاق می­افتد. به عبارت دیگر باید به صورت خودکار رفتار را بر اساس نوع کلاس تصحیح کنیم. این روش در مقابل بررسی نوع داده‌ای برای انجام رفتار مناسب می­باشد.

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

public class ShapeWithoutPolymorphism
    {
        public double X { get; set; }
        public double Y { get; set; }
        public double Z { get; set; }
        public double Area(string shapeType)
        {
            switch (shapeType)
            {
                case "square":
                    return X * Y;
                case "rectangle":
                    return X * Y;
                case "trapze":
                    return (X + Z) * Y / 2;
                default:
                    return 0;
            }
        }
    }

با استفاده از چندریختی، طراحی به این صورت در خواهد آمد:

  public abstract class Shape
    {
        public double X { get; set; }
        public double Y { get; set; }
        public virtual double Area()
        {
            return X * Y;
        }
    }
    public class Rectangle : Shape
    {
        // No need to override
    }
    public class Square : Shape
    {
        // No need to override
    }
    public class Trapze : Shape
    {
        public double Z { get; set; }
        public override double Area()
        {
            return (X + Z) * Y / 2;

        }
    }


مصنوع خالص ( Pure Fabrication )

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

برای تشخیص زمان استفاده از این اصل میتوان گفت زمانیکه رفتاری را نمیدانیم به کدام کلاس واگذار کنیم، کلاس جدیدی را ایجاد میکنیم. در اینجا بجای آنکه به زور مسئولیتی را به کلاس نامربوطی بچسبانیم، آنرا به کلاس جدیدی که فقط رفتاری را دارد، منتقل میکنیم. با اینکار انسجام کلاس‌ها را حفظ کرده‌ایم و هیچ اشکالی ندارد که کلاسی بدون داده بوده و فقط متد داشته باشد. اگر به یاد داشته باشید، در اصل واسطه گری (Indirection ) کلاس جدیدی برای ایجاد ارتباط ساختیم. در حقیقت مسئولیت برقراری ارتباط بین مؤلفه‌ها را به کلاس دیگری واگذار کردیم که چنانچه میبینید، بدون آنکه بدانیم، برای حل مشکل از اصل مصنوع خالص استفاده کردیم.

در مثال زیر این مساله مشهود است:

public class User
    {
        public int Id { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
    }

    public class LibraryManagement
    {
        public User CurrentUser { get; set; }
        public void AddBookToLibrary(int bookId)
        {
            // check for CurrentUser authority:
            // not user's responsibility nor LibraryManagement
        }
        public void RearrangeBook(int bookId, int shelfId)
        {
            // check for CurrentUser authority
            // not user's responsibility nor LibraryManagement
        }
    }
    public class UserManagement
    {
        public User CurrentUser { get; set; }
        public void AddUser(string name)
        {
            // check for CurrentUser authority:
            // not user's responsibility nor UserManagement
        }
        public void ChangeUserRole(int userId, int roleId)
        {
            // check for CurrentUser authority
            // not user's responsibility nor UserManagement
        }
    }
    public class AuthorizationService
    {
        public bool IsAuthorized(int userId, int roleId)
        {
            // get user roles from data base
            // return true if user has the authority
        }
    }

عملیات بررسی مجوز‌ها باید در کلاس جدیدی به نام AuthorizationService ارائه شود. بدین صورت تمام قسمت‌ها، از این کد بدون وابستگی اضافی میتوانند استفاده کنند.


حفاظت از تاثیر تغییرات ( Protected Variations )

این اصل میگوید که کلاس‌ها باید از تغییرات یکدیگر مصون بمانند. در واقع این اصل غایت یک طراحی خوب است. تمام اصولی را که تا به حال بررسی کرده‌ایم، به منظور دستیابی به چنین رفتاری از طراحی بوده‌است. بدین منظور باید از اصول Open/Closed برای واسط­ها، چند ریختی در توارث و ... استفاده کرد تا از تاثیرات زنجیره‌ای تغییرات در امان بمانیم.

مطالب
بدست آوردن اندازه ViewState‌ یک صفحه ASP.Net

یکی از روش‌های معمولی که برای بدست آوردن اندازه ViewState یک صفحه در ASP.Net بکار می‌رود، نمایش صفحه، مراجعه به سورس آن و سپس ذخیره مقدار فیلد مخفی ViewState به صورت یک فایل متنی و مراجعه به اندازه آن فایل است!
راه بهتری هم وجود دارد که به شرح زیر است:
زمانیکه قرار است ViewState به صفحه اضافه شود، روال رخداد گردان SavePageStateToPersistenceMedium اجرا خواهد شد. نحوه تحریف آن برای بدست آوردن اندازه ViewState به صورت زیر است (چند سطر زیر را به وب فرم خود اضافه کنید):

using System;
using System.Diagnostics;
using System.IO;
using System.Web.UI;

protected override void SavePageStateToPersistenceMedium(object state)
{
if (Debugger.IsAttached)
{
using (MemoryStream stream = new MemoryStream())
{
LosFormatter formatter = new LosFormatter();
formatter.Serialize(stream, state);
int size = stream.Capacity / 1024;
if (size > 32)
{
throw new Exception("لطفا به وضعیت ویوو استیت این صفحه رسیدگی نمائید");
}
}
}
base.SavePageStateToPersistenceMedium(state);
}

در اینجا اگر در حال دیباگ برنامه باشید و همچنین حجم ViewState از 32 کیلوبایت بیشتر شد، استثنایی جهت گوشزد گردن این مطلب به شما، تولید می‌شود.
اگر علاقمند بودید که این روال را به تمامی صفحات اضافه کنید می‌شود یک کلاس جدید ایجاد کرد و این کلاس را از کلاس استاندارد Page به ارث برد ( برای نمونه public class MyPage : Page ) . سپس همان چند سطر فوق را به آن اضافه نمائید.
اکنون تغییری که در کدها باید صورت گیرد به صورت زیر است، برای مثال تبدیل:

public partial class WebForm4 : System.Web.UI.Page

به :

public partial class WebForm4 : MyPage


مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت چهارم
در قسمت قبل تشخیص تغییرات توسط Web API را بررسی کردیم. در این قسمت نگاهی به پیاده سازی Change-tracking در سمت کلاینت خواهیم داشت.


ردیابی تغییرات در سمت کلاینت توسط Web API

فرض کنید می‌خواهیم از سرویس‌های REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین می‌خواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیت‌ها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی داده‌ها توسط مدل Code-First انجام می‌شود.

در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویس‌های ارائه شده توسط پروژه Web API را فراخوانی می‌کند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی می‌کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه می‌کند. می‌خواهیم مدل‌ها و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
  • کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
  • کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیت‌ها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه می‌کند. کلاینت‌ها هنگام ویرایش آبجکت موجودیت‌ها باید این فیلد را مقدار دهی کنند. همانطور که می‌بینید این خاصیت از نوع TrackingState enum مشتق می‌شود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیت‌ها بدین روش، وابستگی‌های EF را برای کلاینت از بین می‌بریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگی‌های بخصوصی معرفی می‌شدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور می‌دهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity
{
    protected BaseEntity()
    {
        TrackingState = TrackingState.Nochange;
    }

    public TrackingState TrackingState { get; set; }
}

public enum TrackingState
{
    Nochange,
    Add,
    Update,
    Remove,
}
  • کلاس‌های موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Company { get; set; }
    public virtual ICollection<Phone> Phones { get; set; }
}

public class Phone : BaseEntity
{
    public int PhoneId { get; set; }
    public string Number { get; set; }
    public string PhoneType { get; set; }
    public int CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیت‌های جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی می‌کنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext
{
    public Recipe4Context() : base("Recipe4ConnectionString") { }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Phone> Phones { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // Do not persist TrackingState property to data store
        // This property is used internally to track state of
        // disconnected entities across service boundaries.
        // Leverage the Custom Code First Conventions features from Entity Framework 6.
        // Define a convention that performs a configuration for every entity
        // that derives from a base entity class.
        modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState));
        modelBuilder.Entity<Customer>().ToTable("Customers");
        modelBuilder.Entity<Phone>().ToTable("Phones");
}
}
  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe4ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال می‌کند و به JSON serializer دستور می‌دهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیت‌های Customer و PhoneNumber بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
            Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینت‌ها ارائه می‌شود را به مقادیر متناظر کامپوننت‌های ردیابی EF تبدیل می‌کند.
public static EntityState Set(TrackingState trackingState)
{
    switch (trackingState)
    {
        case TrackingState.Add:
            return EntityState.Added;
        case TrackingState.Update:
            return EntityState.Modified;
        case TrackingState.Remove:
            return EntityState.Deleted;
        default:
            return EntityState.Unchanged;
    }
}
  • در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController
{
    // GET api/customer
    public IEnumerable<Customer> Get()
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).ToList();
        }
    }

    // GET api/customer/5
    public Customer Get(int id)
    {
        using (var context = new Recipe4Context())
        {
            return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id);
        }
    }

    [ActionName("Update")]
    public HttpResponseMessage UpdateCustomer(Customer customer)
    {
        using (var context = new Recipe4Context())
        {
            // Add object graph to context setting default state of 'Added'.
            // Adding parent to context automatically attaches entire graph
            // (parent and child entities) to context and sets state to 'Added'
            // for all entities.
            context.Customers.Add(customer);
            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                entry.State = EntityStateFactory.Set(entry.Entity.TrackingState);
                if (entry.State == EntityState.Modified)
                {
                    // For entity updates, we fetch a current copy of the entity
                    // from the database and assign the values to the orginal values
                    // property from the Entry object. OriginalValues wrap a dictionary
                    // that represents the values of the entity before applying changes.
                    // The Entity Framework change tracker will detect
                    // differences between the current and original values and mark
                    // each property and the entity as modified. Start by setting
                    // the state for the entity as 'Unchanged'.
                    entry.State = EntityState.Unchanged;
                    var databaseValues = entry.GetDatabaseValues();
                    entry.OriginalValues.SetValues(databaseValues);
                }
            }

        context.SaveChanges();
    }

    return Request.CreateResponse(HttpStatusCode.OK, customer);
}

    [HttpDelete]
    [ActionName("Cleanup")]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe4Context())
        {
            context.Database.ExecuteSqlCommand("delete from phones");
            context.Database.ExecuteSqlCommand("delete from customers");
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
حال اپلیکیشن کلاینت (برنامه کنسول) را می‌سازیم که از این سرویس استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program
{
    private HttpClient _client;
    private Customer _bush, _obama;
    private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up completes
        await program.CleanupAsync();
        program.CreateFirstCustomer();
        // do not proceed until customer is added
        await program.AddCustomerAsync();
        program.CreateSecondCustomer();
        // do not proceed until customer is added
        await program.AddSecondCustomerAsync();
        // do not proceed until customer is removed
        await program.RemoveFirstCustomerAsync();
        // do not proceed until customers are fetched
        await program.FetchCustomersAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") };
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue
        ("application/json"));
    }
    private async Task CleanupAsync()
    {
        // call the cleanup method from the service
        _response = await _client.DeleteAsync("api/customer/cleanup/");
    }

    private void CreateFirstCustomer()
    {
        // create customer #1 and two phone numbers
        _bush = new Customer
        {
            Name = "George Bush",
            Company = "Ex President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _whiteHousePhone = new Phone
        {
            Number = "212 222-2222",
            PhoneType = "White House Red Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bushMobilePhone = new Phone
        {
            Number = "212 333-3333",
            PhoneType = "Bush Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _bush.Phones.Add(_whiteHousePhone);
        _bush.Phones.Add(_bushMobilePhone);
    }

    private async Task AddCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _bush = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _bush.Name, _bush.Phones.Count);
            foreach (var phoneType in _bush.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondCustomer()
    {
        // create customer #2 and phone numbers
        _obama = new Customer
        {
            Name = "Barack Obama",
            Company = "President",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        _obamaMobilePhone = new Phone
        {
            Number = "212 444-4444",
            PhoneType = "Obama Mobile Phone",
            // set tracking state to 'Add' to generate a SQL Insert statement
            TrackingState = TrackingState.Add,
        };
        // set tracking state to 'Modifed' to generate a SQL Update statement
        _whiteHousePhone.TrackingState = TrackingState.Update;
        _obama.Phones.Add(_obamaMobilePhone);
        _obama.Phones.Add(_whiteHousePhone);
    }

    private async Task AddSecondCustomerAsync()
    {
        // construct call to invoke UpdateCustomer action method in Web API service
        _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created customer entity from service, which will include
            // database-generated Ids for all entities
            _obama = await _response.Content.ReadAsAsync<Customer>();
            _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId);
            Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)",
            _obama.Name, _obama.Phones.Count);
            foreach (var phoneType in _obama.Phones)
            {
                Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType);
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
    }

    private async Task RemoveFirstCustomerAsync()
    {
        // remove George Bush from underlying data store.
        // first, fetch George Bush entity, demonstrating a call to the
        // get action method on the service while passing a parameter
        var query = "api/customer/" + _bush.CustomerId;
        _response = _client.GetAsync(query).Result;

        if (_response.IsSuccessStatusCode)
        {
            _bush = await _response.Content.ReadAsAsync<Customer>();
            // set tracking state to 'Remove' to generate a SQL Delete statement
            _bush.TrackingState = TrackingState.Remove;
            // must also remove bush's mobile number -- must delete child before removing parent
            foreach (var phoneType in _bush.Phones)
            {
                // set tracking state to 'Remove' to generate a SQL Delete statement
                phoneType.TrackingState = TrackingState.Remove;
            }
            // construct call to remove Bush from underlying database table
            _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter());
            if (_response.IsSuccessStatusCode)
            {
                Console.WriteLine("Removed {0} from database", _bush.Name);
                foreach (var phoneType in _bush.Phones)
                {
                    Console.WriteLine("Remove {0} from data store", phoneType.PhoneType);
                }
            }
            else
                Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }

    private async Task FetchCustomersAsync()
    {
        // finally, return remaining customers from underlying data store
        _response = await _client.GetAsync("api/customer/");
        if (_response.IsSuccessStatusCode)
        {
            var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>();
            foreach (var customer in customers)
            {
                Console.WriteLine("Customer {0} has {1} Phone Numbers(s)",
                customer.Name, customer.Phones.Count());
                foreach (var phoneType in customer.Phones)
                {
                    Console.WriteLine("Phone Type: {0}", phoneType.PhoneType);
                }
            }
        }
        else
        {
            Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase);
        }
    }
}

  • در آخر کلاس‌های Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایه‌های مختلف اپلیکیشن به اشتراک گذاشته شوند.

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








شرح مثال جاری

با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت می‌کند. در این مرحله سایت در حال اجرا است و سرویس‌ها قابل دسترسی هستند.

سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت می‌کنیم و از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی می‌کنیم. این فراخوانی تمام داده‌های پیشین را حذف می‌کند.

در قدم بعدی یک مشتری بهمراه دو شماره تماس می‌سازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی می‌کنیم تا کامپوننت‌های Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.

سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی می‌کنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت می‌کند و آن را به context جاری اضافه می‌نماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه می‌شود و EF شروع به ردیابی تغییرات آن می‌کند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.

قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده می‌کنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه می‌کند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه می‌دهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیت‌ها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل می‌کنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام می‌شود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر می‌دهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت می‌کنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت می‌کند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص می‌دهیم. پشت پرده، کامپوننت‌های EF Change-tracking بصورت خودکار تفاوت‌های مقادیر اصلی و مقادیر ارسالی را تشخیص می‌دهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری می‌کنند. فراخوانی‌های بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.

در اپلیکیشن کلاینت عملیات افزودن، بروز رسانی و حذف موجودیت‌ها توسط مقداردهی خاصیت TrackingState را نمایش داده ایم.

متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل می‌کند و آبجکت‌ها را به موتور change-tracking ارسال می‌کند که نهایتا منجر به تولید دستورات لازم SQL می‌شود.

نکته: در اپلیکیشن‌های واقعی بهتر است کد دسترسی داده‌ها و مدل‌های دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت می‌تواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی می‌توانید برای تمام موجودیت‌ها استفاده کنید.

نظرات مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت دوم - الگوی Service Locator
یک نکته‌ی تکمیلی: طراحی یک کلاس ServiceLocator برای NET Core.

گاهی از اوقات مجبور به کار با کتابخانه‌هایی هستید که برای کار با تزریق وابستگی‌ها طراحی نشده‌اند. برای مثال این کتابخانه‌ها کلاسی را از شما دریافت می‌کنند، این کلاس را خودشان وهله سازی کرده و در نهایت استفاده خواهند کرد. چون وهله سازی این کلاس در اختیار شما نیست و همچنین کتابخانه‌ی فراخوان نیز از تزریق وابستگی‌های در سازنده‌ی کلاس دریافتی، پشتیبانی نمی‌کند، تنها راه حل باقیمانده، استفاده از الگوی Service Locator خواهد بود. برای این منظور می‌توانید از دو کلاس زیر کمک بگیرید:
using System;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;

namespace Utils
{
    public static class ServiceLocatorProvider
    {
        private static readonly Lazy<IServiceProvider> _serviceProviderBuilder =
            new Lazy<IServiceProvider>(GetServiceProvider, LazyThreadSafetyMode.ExecutionAndPublication);

        /// <summary>
        /// A lazy loaded thread-safe singleton
        /// </summary>
        public static IServiceProvider Current { get; } = _serviceProviderBuilder.Value;

        private static IServiceProvider GetServiceProvider()
        {
            var services = new ServiceCollection();
            ConfigureServices(services);
            return services.BuildServiceProvider();
        }

        private static void ConfigureServices(IServiceCollection services)
        {
            // TODO: add other services here ... services.AddSingleton ....
        }
    }

    public static class ServiceLocator
    {
        public static object GetService(Type serviceType)
        {
            return ServiceLocatorProvider.Current.GetService(serviceType);
        }

        public static TService GetService<TService>()
        {
            return ServiceLocatorProvider.Current.GetService<TService>();
        }

        public static object GetRequiredService(Type serviceType)
        {
            return ServiceLocatorProvider.Current.GetRequiredService(serviceType);
        }

        public static TService GetRequiredService<TService>()
        {
            return ServiceLocatorProvider.Current.GetService<TService>();
        }

        public static void RunScopedService<T, S>(Action<S, T> callback)
        {
            using (var serviceScope = ServiceLocatorProvider.Current.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetRequiredService<S>();

                callback(context, serviceScope.ServiceProvider.GetRequiredService<T>());
                if (context is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
        }

        public static void RunScopedService<S>(Action<S> callback)
        {
            using (var serviceScope = ServiceLocatorProvider.Current.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetRequiredService<S>();
                callback(context);
                if (context is IDisposable disposable)
                {
                    disposable.Dispose();
                }
            }
        }

        public static T RunScopedService<T, S>(Func<S, T> callback)
        {
            using (var serviceScope = ServiceLocatorProvider.Current.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetRequiredService<S>();
                return callback(context);
            }
        }
    }
}
در اینجا باید متد ConfigureServices کلاس ServiceLocatorProvider را همانند قبل تنظیم و تعاریف سرویس‌های مدنظر خود را اضافه کنید. سپس در هر قسمتی از برنامه می‌توانید از متدهایی مانند ()<ServiceLocator.GetRequiredService<TService استفاده نمائید. در مورد متدهای RunScopedService آن در قسمت سوم بیشتر بحث شده‌است.
مطالب
چند نکته اضافه برای Refactoring
Refactoring عامل خوانایی کد و در بسیاری از مواقع، سبب بالاتر رفتن کارآیی برنامه است. در واقع حتی بسیاری از قوانین Refactoring خود یک الگوی طراحی به شمار می‌آیند. در این مقاله به تعدادی از مباحث Refactoring می‌پردازیم:

یک: به جای بازگرداندن شماره خطا، از استثناءها استفاده کنید. نمونه زیر را ببینید:
public int ReturnErrorCodes(int n1)
{
         if(n1==0)
               return -1;
          if(n1<0)
                 return -2;
            if(n1>_max)
                return -3;
            return n1;
}
همانطور که می‌بینید در کد بالا شماره‌های خطا بازگشت داده می‌شوند. در این حالت می‌توانیم آن‌ها را با استثناءها جایگزین کنیم:
public int ReturnErrorCodes(int n1)
{
         if(n1==0)
                throw new ZeroException();
          if(n1<0)
                 throw new MinException();
            if(n1>_max)
                throw new MaxException();
            return n1;
}

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

 دو. یک شیء کامل را بازگردانید.
نمونه کد زیر را ببینید:
Public Void Draw()
{
        var x=_pointDetector.X;
        var y=_pointerDetector.Y;
        _rectangle.Draw(x,y);
}
در این شیوه، هر چقدر متغیرهای برگشتی افزایش پیدا کنند، اهمیت استفاده از آن بیشتر می‌شود. برای حل این مسئله باید به جای برگرداندن تک تک مقادیر، همه آن‌ها را در قالب یک شیء برگردانید:
public Void Draw()
{
     var point = _pointDetector.Point;
     _rectangle.Draw(point);
}
از مزایای استفاده‌ی از الگو، این است که تعداد خطوط شما در بدنه اصلی، کاهش می‌یابد و اگر در آینده نیاز به افزایش تعداد خروجی‌ها باشد، لازم نیست که بدنه‌ی اصلی را هم تغییر بدهید و مرتب کدی یا خطی را به آن اضافه کنید.

سه
. شروط یکسان را تابع نویسی کنید. گاهی از اوقات مانند کد زیر پیش می‌آید که خروجی چندین شرط شما، خروجی یکسانی دارند. جهت این کار می‌توانید تمام این شرط‌ها را به یک تابع یا متد دیگر منتقل کنید:
public void Conditions(){
     if(a==0)
             return 0;
     if(b==0)
             return 0;
     if(c==0)
             return 0;
     if(d==0)
             return 0;
}
به جای آن می‌نویسیم:
public void Conditions()
{
          if(allConditions())
               return 0;
}
بدین صورت بدنه اصلی خلاصه‌تر شده و شرط‌ها به متدی با نامی که هدف آن را مشخص می‌کند انتقال می‌یابند.

چهار. چند خط کد شرطی را در یک شرط ننویسید و آنها را به متغیرهای جداگانه انتساب دهید. نمونه زیر را ببینید:
public void renderBanner() {
  if ((platform.toUpperCase().indexOf("MAC") > -1) &&
       (browser.toUpperCase().indexOf("IE") > -1) &&
        wasInitialized() && resize > 0 )
  {
    // do something
  }
}
در این حالت بهتر است هر شرط به یک خط و یک متغیر انتقال یابد تا خطوط تمیزتر و خلاصه‌تر و قابل فهم‌تری داشته باشیم:
public void renderBanner() {
  bool isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
  bool isIE = browser.toUpperCase().indexOf("IE") > -1;
  bool wasResized = resize > 0;

  if (isMacOs && isIE && wasInitialized() && wasResized) {
    // do something
  }
}

پنج
. معرفی اکستنشن محلی: گاهی اوقات از کلاس‌هایی استفاده می‌کنید که شامل متد یا خاصیتی که احتیاج دارید نیستند و دسترسی به سورس آن کلاس هم امکان پذیر نیست یا هزینه سنگینی دارد. در این صورت می‌توانید از یک زیر کلاس یا Wrapper Class استفاده کنید.
به عنوان مثال متد NextDay جایگاه آن در DateTime است. برای افزودن این خاصیت دو راه دارید که یک زیر کلاس از DateTime ایجاد کنید و متدی را به آن اضافه کنید یا اینکه کلاس DateTime را در یک کلاس دیگر بپیچانید (محصور کنید).
public class Globalization:DateTime
{
     public int NextDay()
     {
      }
}
یا
public class Globalization
{
    public DateTime DT{get;set;}
     public int NextDay()
     {
      }
}

پی نوشت:
اینکه کلاس dateTime قابل ارث بری است یا خیر اهمیتی ندارد. تنها به عنوان مثال ذکر شده است.
مزیت این روش این است که شما در طول برنامه از یک کلاس یکسان استفاده می‌کنید و با همین یک کلاس به همه موارد دسترسی دارید و باعث وجود کد یکتایی می‌شوید و به جای اینکه مرتبا از کلاس‌های مختلف استفاده کنید، از یک نام مشخص استفاده می‌کنید و خوانایی کد را در کل برنامه بالا می‌برید.