مطالب
کار با نوع داده‌ی HierarchyID توسط Entity framework
نوع داده‌ی HierarchyID به همراه SQL Server 2008 برای کار با داده‌هایی با ساختار درختی ارائه شد. در حال حاضر هیچکدام از ORMهای موجود، پشتیبانی رسمی را از این نوع داده به عمل نمی‌آورند؛ اما با توجه به سورس باز بودن Entity framework، یک Fork مستقل از آن تهیه شده‌است و این نوع داده‌ی جدید به همراه متدهای مرتبط با آن، به این Fork اضافه شده‌اند.
- اصل Fork در اینجا
- تاریخچه‌ی این Fork غیر رسمی در اینجا
- بسته‌ی نیوگت آن در اینجا

چون تیم EF در نگارش فعلی این کتابخانه حاضر به افزودن این نوع جدید نشده‌است، بنابراین بجای بسته‌ی اصلی Entity framework نیاز است بسته‌ی EntityFrameworkWithHierarchyId را نصب کنید.
 PM> install-package EntityFrameworkWithHierarchyId

یک تذکر مهم:
چون امضای دیجیتال این بسته، با امضای دیجیتال بسته‌ی اصلی EF یکی نیست، اگر پروژه‌ی شما صرفا از EF استفاده می‌کند، مشکلی نخواهید داشت. اما اگر برای مثال از ASP.NET Identity کامپایل شده‌ی برای کار با EF اصلی استفاده کنید، پیام یافت نشدن DLL مرتبط را دریافت خواهید کرد.


تعریفی مدلی با خاصیتی از نوع جدید HierarchyId

public class Employee
{
    public int Id { get; set; }
 
    [Required, MaxLength(100)]
    public string Name { get; set; }
 
    [Required]
    public HierarchyId Node { get; set; } // نوع داده جدید
}
در اینجا مدلی را ملاحظه می‌کنید که از نوع داده‌ی جدید HierarchyId استفاده می‌کند. همانطور که عنوان شد این نوع در بسته‌ی EntityFrameworkWithHierarchyId موجود است.


تعریف Context و مقدار دهی اولیه‌ی آن

در این حالت Context برنامه به همراه تنظیمات اولیه‌ی Migrations آن یک چنین شکلی را پیدا خواهد کرد:
public class MyContext : DbContext
{
    public DbSet<Employee> Employees { get; set; }
 
    public MyContext()
        : base("Connection1")
    {
        this.Database.Log = log => Console.WriteLine(log);
    }
}
 
public class Configuration : DbMigrationsConfiguration<MyContext>
{
    public Configuration()
    {
        AutomaticMigrationsEnabled = true;
        AutomaticMigrationDataLossAllowed = true;
    }
 
    protected override void Seed(MyContext context)
    {
        if (context.Employees.Any())
            return;
 
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD NodePath as Node.ToString() persisted");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD Level AS Node.GetLevel() persisted");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD ManagerNode as Node.GetAncestor(1) persisted");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD ManagerNodePath as Node.GetAncestor(1).ToString() persisted");
 
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees] ADD CONSTRAINT [UK_EmployeeNode] UNIQUE NONCLUSTERED (Node)");
        context.Database.ExecuteSqlCommand(
            "ALTER TABLE [dbo].[Employees]  WITH CHECK ADD CONSTRAINT [EmployeeManagerNodeNodeFK] " +
            "FOREIGN KEY([ManagerNode]) REFERENCES [dbo].[Employees] ([Node])");
 
        context.Employees.Add(new Employee { Name = "Root", Node = new HierarchyId("/") });
        context.Employees.Add(new Employee { Name = "Emp1", Node = new HierarchyId("/1/") });
        context.Employees.Add(new Employee { Name = "Emp2", Node = new HierarchyId("/2/") });
        context.Employees.Add(new Employee { Name = "Emp3", Node = new HierarchyId("/1/1/") });
        context.Employees.Add(new Employee { Name = "Emp4", Node = new HierarchyId("/1/1/1/") });
        context.Employees.Add(new Employee { Name = "Emp5", Node = new HierarchyId("/2/1/") });
        context.Employees.Add(new Employee { Name = "Emp6", Node = new HierarchyId("/1/2/") });
 
        base.Seed(context);
    }
}
در اینجا نحوه‌ی تعریف رکوردهای جدید مبتنی بر HierarchyId را مشاهده می‌کنید که توسط آن‌ها تعدادی کارمند، در یک سازمان فرضی ثبت شده‌اند.
همچنین چند فیلد محاسباتی نیز بر اساس امکانات توکار SQL Server اضافه شده‌اند. متدهایی مانند ToString، GetLevel، GetAncestor و امثال آن جزئی از پیاده سازی توکار SQL Server هستند. همچنین این متدها توسط کتابخانه‌ی EntityFrameworkWithHierarchyId نیز ارائه شده‌اند.


کوئری نویسی

مرتب سازی رکوردها بر اساس HierarchyId آن‌ها

using (var context = new MyContext())
{
    Console.WriteLine("\ngetItems OrderByDescending(employee => employee.Node)");
 
    var employees = context.Employees.OrderByDescending(employee => employee.Node).ToList();
    foreach (var employee in employees)
    {
        Console.WriteLine("{0} {1}", employee.Id, employee.Node);
    }
 }
با این خروجی
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    ORDER BY [Extent1].[Node] DESC


6 /2/1/
3 /2/
7 /1/2/
5 /1/1/1/
4 /1/1/
2 /1/
1 /


یافتن یک HierarchyId خاص و سپس یافتن کلیه‌ی فرزندان آن در یک سطح پایین‌تر

using (var context = new MyContext())
{
    Console.WriteLine("\nGetAncestor(1) of /1/");
 
    var firstItem = context.Employees.Single(employee => employee.Node == new HierarchyId("/1/"));
    foreach (var item in context.Employees.Where(employee => firstItem.Node == employee.Node.GetAncestor(1)))
    {
        Console.WriteLine("{0} {1}", item.Id, item.Name);
    }
}
این کوئری را به این شکل نیز می‌توان عنوان کرد: یافتن یک HierarchyId و سپس یافتن کلیه نودهایی که والدشان (GetAncestor) این HierarchyId است. عدد یک در اینجا مشخص کننده‌ی Level یا سطح است.
با این خروجی:
SELECT TOP (2)
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE cast('/1/' as hierarchyid) = [Extent1].[Node]

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE (@p__linq__0 = ([Extent1].[Node].GetAncestor(1))) OR ((@p__linq__0 IS
NULL) AND ([Extent1].[Node].GetAncestor(1) IS NULL))
-- p__linq__0: '/1/' (Type = Object)

4 Emp3
7 Emp6

کوئری‌های فوق را می‌توان بجای استفاده از متد GetAncestor، با استفاده از متد IsDescendantOf به شکل زیر نیز نوشت:
var list = context.Employees.Where(
          employee => employee.Node.IsDescendantOf(new HierarchyId("/1/")) &&
                              employee.Node.GetLevel() == 2).ToList();
با این خروجی SQL (یک کوئری بجای دو کوئری):
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[Node] AS [Node]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE (([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1) 
    AND (2 = ([Extent1].[Node].GetLevel()))


جابجا کردن نودها توسط متد GetReparentedValue

در کوئری ذیل، تمامی فرزندان ریشه‌ی /1/ یافت شده و سپس والد آن‌ها به صورت پویا تغییر داده می‌شود:
var items = context.Employees.Where(employee => employee.Node.IsDescendantOf(new HierarchyId("/1/")))
    .Select(employee => new
    {
        Id = employee.Id,
        OrigPath = employee.Node,
        ReparentedValue = employee.Node.GetReparentedValue(new HierarchyId("/1/"), HierarchyId.GetRoot()),
        Level = employee.Node.GetLevel()
    }).ToList();
 
foreach (var item in items)
{
    Console.WriteLine("Id:{0}; OrigPath:{1}; ReparentedValue:{2}; Level:{3}", item.Id, item.OrigPath, item.ReparentedValue, item.Level);
}
با این خروجی
SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Node] AS [Node],
    [Extent1].[Node].GetReparentedValue(cast('/1/' as hierarchyid), hierarchyid::GetRoot()) AS [C1],
    [Extent1].[Node].GetLevel() AS [C2]
    FROM [dbo].[Employees] AS [Extent1]
    WHERE ([Extent1].[Node].IsDescendantOf(cast('/1/' as hierarchyid))) = 1


Id:2; OrigPath:/1/; ReparentedValue:/; Level:1
Id:4; OrigPath:/1/1/; ReparentedValue:/1/; Level:2
Id:5; OrigPath:/1/1/1/; ReparentedValue:/1/1/; Level:3
Id:7; OrigPath:/1/2/; ReparentedValue:/2/; Level:2

کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
HierarcyIdTests.zip
مطالب
بهبود کارآیی نمایش لیست‌ها در Blazor با استفاده از دایرکتیو key@
اگر پیشتر با React کار کرده باشید، احتمالا چنین پیام خطایی را دریافت کرده‌اید:


در اینجا React عنوان می‌کند که هر عنصر از لیستی را که در حال نمایش آن هستید، باید به همراه یک key، ارائه دهید. اما ... این key چیست؟
زمانیکه حالت کامپوننتی تغییر می‌کند (شیء یا اشیایی که به عناصر UI متصل هستند، تغییر می‌کنند)، React، درخت جدیدی از اشیایی را که باید رندر شوند، تولید می‌کند. اکنون React باید محاسبه کند که چه عناصری نسبت به درخت فعلی که در حال نمایش است، تغییر کرده‌اند تا فقط آن‌ها را به DOM اصلی اعمال کند؛ تا این تغییرات به کاربر نمایش داده شوند و ... این کار را هم به خوبی انجام می‌دهد. پس مشکل با لیست‌ها چیست که نیاز به key دارند؟
فرض کنید رندر لیستی، خروجی زیر را تولید می‌کند:
<li>Item 1</li>
<li>Item 2</li>
اکنون یک المان را به انتهای این لیست اضافه می‌کنیم:
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
در این حالت React به خوبی تشخیص می‌دهد که المان سومی به لیست اضافه شده‌است و فقط آن‌را رندر می‌کند؛ بجای اینکه کل لیست را مجددا رندر کند. اما اگر نحوه‌ی اضافه شدن المان چهارمی به لیست جدید، به صورت زیر باشد:
<li>Item 0</li> // <= New item
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
یعنی این المان در ابتدای لیست، اضافه شده باشد، اینبار المان اول لیست سه‌تایی قبلی را با المان اول لیست چهارتایی جدید مقایسه می‌کند (مقایسه‌ی بر اساس ایندکس). چون این دو یکی نیستند، کل لیست جدید را مجددا رندر خواهد کرد؛ و در این حالت دیگر نمی‌تواند تشخیص دهد که المان‌هایی در این لیست هستند که با قبل تفاوتی ندارند.
راه‌حل React برای تشخیص منحصربفرد بودن المان‌های یک لیست و یا آرایه، استفاده از خاصیت key است:
<li key={0}>Item 0</li> // <= New item
<li key={1}>Item 1</li>
<li key={2}>Item 2</li>
<li key={3}>Item 3</li>
در اینجا هر آیتم لیست را با یک key منحصربفرد مشخص می‌کنیم. در این حالت React دقیقا می‌تواند محاسبه کند، عنصری که در آرایه‌ی در حال رندر تغییر کرده‌است، کدام است و فقط آن‌را در DOM نهایی به روز رسانی می‌کند؛ و نه اینکه کل لیست را مجددا رندر کند.

این نکات چه ربطی به Blazor دارند؟!
واقعیت این است که Blazor، همان نسخه‌ی مایکروسافتی React است (!) و این خاصیت key، در Blazor نیز تحت عنوان key directive@ وجود دارد و دقیقا مفهوم آن نیز با توضیحاتی که در مورد React داده شد، یکی است.
زمانیکه Blazor صفحه‌ای را رندر می‌کند، ابتدا یک DOM مشخصی را تولید خواهد کد. سپس با تغییر State یک کامپوننت، DOM جدیدی را محاسبه کرده و آن‌را با DOM فعلی مقایسه می‌کند و در نهایت diff تولیدی را به DOM موجود، در جهت نمایش تغییرات، اعمال خواهد کرد.
بنابراین الگوریتم diff باید اضافات، به روز رسانی‌ها و حذف‌های صورت گرفته‌ی در UI را تشخیص داده و فقط قسمت‌های تغییر یافته را جهت به روز رسانی بهینه‌ی UI، به آن اعمال کند. این الگوریتم diff به صورت پیش‌فرض از ایندکس المان‌ها برای مقایسه‌ی آن‌ها استفاده می‌کند. هرچند این روش در بسیاری از حالات، بهینه‌ترین روش است، اما در مورد لیست‌ها خیر؛ که توضیحات آن‌را با مثالی در مورد React، در ابتدای بحث بررسی کردیم. برای مثال اگر شیءای به انتهای لیست اضافه شود، هر المانی را که پس از این ایندکس قرار گرفته باشد، تغییر یافته درنظر گرفته و آن‌را مجددا رندر می‌کند. به همین جهت است که اگر المانی به ابتدای یک لیست اضافه شود، اینبار کل لیست را مجددا رندر می‌کند (چون تمام ایندکس‌های اشیاء موجود در لیست، تغییر کرده‌اند)؛ صرفنظر از اینکه عناصری از این لیست، پیشتر در UI رندر شده‌اند و نیازی به رندر مجدد، ندارند.


یک مثال: بررسی نحوه‌ی رندر لیستی از اشیاء در Blazor

در اینجا کدهای کامل کامپوننتی را مشاهده می‌کنید که یک لیست ساده را در ابتدا رندر کرده و هر بار که بر روی دکمه‌ی افزودن یک شخص کلیک می‌شود، شخص جدیدی را به ابتدای لیست اضافه می‌کند:
@page "/"

<button class="btn btn-primary" @onclick="addPerson">Add Person</button>

<ul class="mt-5">
    @foreach (var person in People)
    {
        <li>@person.Id, @person.Name</li>
    }
</ul>

@code{

    List<Person> People = new()
    {
        new() { Id = 1, Name = "User 1" },
        new() { Id = 2, Name = "User 2" },
    };
    int LastId = 2;

    void addPerson()
    {
        People.Insert(0, new() { Id = ++LastId, Name = $"User {LastId}" });
    }

    class Person
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}
در این حالت اگر به لیست المان‌های نمایش داده شده‌ی در ابزار‌های توسعه دهندگان مرورگر مراجعه کنیم، با هر بار کلیک بر روی دکمه افزودن یک شخص جدید، محتوای li‌های نمایش داده شده، ابتدا به رنگ صورتی در آمده و سپس عادی می‌شوند. این تغییر رنگ، به معنای عناصری هستند که هم اکنون مجددا رندر شده‌اند و در UI نهایی تغییر کرده‌اند:


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


تغییر الگوریتم diff محاسبه‌ی تغییرات UI

الگوریتم پیش‌فرض diff، از ایندکس‌های عناصر برای یافتن تغییرات، استفاده می‌کند. با استفاده از دایرکتیو ویژه‌ی key@ می‌توان ایندکس‌های پیش‌فرض را با مقادیری منحصربفرد، بازنویسی کرد، تا اینبار Blazor دقیقا بداند که کدام آیتم، جدید است و کدام‌ها نیازی به رندر مجدد ندارند:
<ul class="mt-5">
    @foreach (var person in People)
    {
        <li @key="person.Id">@person.Id, @person.Name</li>
    }
</ul>
در اینجا تنها تغییر صورت گرفته، اضافه کردن دایرکتیو key@ به هر li در حال رندر است که اینبار مقدار آن، دیگر ایندکس پیش‌فرض عناصر نبوده، بلکه کلید منحصربفرد آن‌ها است.


اگر به تصویر فوق دقت کنید، اینبار فقط li جدیدی که اضافه شده‌است، ابتدا با رنگ صورتی نمایش داده می‌شود و محتوای داخل سایر li ها، دست نخورده باقی مانده‌است؛ یعنی مجددا رندر نشده‌اند.
نظرات مطالب
بررسی چند نکته در مورد ارث بری کلاس‌ها در #C
با کامنت کردن این بخش از کد برنامه :
//Console.WriteLine("====Parent====");
//Parent parent = new Parent();
//parent.Print();

//Console.WriteLine("====Child====");
//Child child = new Child();
//child.Print();
و اجرای کد بخش سوم به تنهایی :
Console.WriteLine("====Parent Via Child====");
Parent pc = new Child();
pc.Print();
خروجی به شکل زیر می‌باشد :(ابتدا سازنده استاتیک فرزند و سپس سازنده استاتیک والد فراخوانی میشود) 


نکته : سازنده‌های استاتیک تنها یکبار فراخوانی می‌شوند بر عکس سازنده‌های غیر استاتیک که به ازای هر بار نمونه سازی از کلاس اجرا می‌شوند.
مطالب
استفاده از Froala WYSIWYG Editor در ASP.NET
چندی قبل، معرفی ادیتور سبک وزن و مناسبی را تحت عنوان RedActor، در این سایت ملاحظه کردید. زمانیکه این‌کار انجام شد، این ادیتور هم رایگان بود و هم سورس آخرین نگارش آن به سادگی در دسترس. بعد از مدتی، هر دو ویژگی یاد شده‌ی RedActor حذف شدند. پس از آن ادیتور مدرن و بسیار مناسب دیگری به نام Froala منتشر شد که هرچند نگارش‌های تجاری هم دارد، اما سورس آخرین نگارش آن برای عموم قابل دریافت است. در ادامه مروری خواهیم داشت بر نحوه‌ی یکپارچه سازی آن با ASP.NET MVC و همچنین ASP.NET Web forms.


دریافت آخرین نگارش Froala WYSIWYG Editor
برای دریافت فایل‌های آخرین نگارش این ادیتور وب می‌توانید به سایت آن، قسمت دریافت فایل‌ها مراجعه نمائید.
http://editor.froala.com/download
و یا به این آدرس مراجعه کنید:
https://github.com/froala/wysiwyg-editor/releases 


ساختار پروژه و نحوه‌ی کپی فایل‌های آن
در هر دو مثالی که فایل‌های آن‌را از انتهای بحث می‌توانید دریافت کنید، این ساختار رعایت شده است:


فایل‌های CSS و فونت‌های آن، در پوشه‌ی Content قرار گرفته‌اند.
فایل‌های اسکریپت و زبان آن (که دارای زبان فارسی هم هست) در پوشه‌ی Scripts کپی شده‌اند.

یک نکته
فایل font-awesome.css را نیاز است کمی اصلاح کنید. مسیر پوشه‌ی فونت‌های آن اکنون با fonts شروع می‌شود.


تنظیمات اولیه

تفاوتی نمی‌کند که از وب فرم‌ها استفاده می‌کنید یا MVC، نحوه‌ی تعریف و افزودن پیش نیازهای این ادیتور به نحو ذیل است:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>

    <link href="Content/font-awesome.css" rel="stylesheet" />
    <link href="Content/froala_editor.css" rel="stylesheet" />

    <script src="Scripts/jquery-1.10.2.min.js"></script>
    <script src="Scripts/froala_editor.min.js"></script>
    <script src="Scripts/langs/fa.js"></script>
</head>
<body>
    <form id="form1" runat="server">
    </form>
</body>
</html>
دو فایل CSS دارد (آیکن‌های آن و همچنین شیوه نامه‌ی اصلی ادیتور) به همراه سه فایل JS (جی‌کوئری، ادیتور و فایل زبان فارسی آن) که باید در فایل master یا layout سایت اضافه شوند.


استفاده از Froala WYSIWYG Editor در ASP.NET MVC

در ادامه نحوه‌ی فعال سازی ادیتور وب Froala را در یک View برنامه‌های ASP.NET MVC ملاحظه می‌کنید:
@{
    ViewBag.Title = "Index";
}

<style type="text/css">
    /*تنظیم فونت پیش فرض ادیتور*/
    .froala-element {
    }
</style>

@using (Html.BeginForm(actionName: "Index", controllerName: "Home"))
{
    @Html.TextArea(name: "Editor1")
    <input type="submit" value="ارسال" />
}

@section Scripts
{
    <script type="text/javascript">
        $(function () {
            $('#Editor1').editable({
                buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily",
                    "fontSize", "color", "formatBlock", "align", "insertOrderedList",
                    "insertUnorderedList", "outdent", "indent", "selectAll", "createLink",
                    "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"],
                inlineMode: false,
                inverseSkin: true,
                preloaderSrc: '@Url.Content("~/Content/img/preloader.gif")',
                allowedImageTypes: ["jpeg", "jpg", "png"],
                height: 300,
                language: "fa",
                direction: "rtl",
                fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"],
                autosave: true,
                autosaveInterval: 2500,
                saveURL: '@Url.Action("FroalaAutoSave", "Home")',
                saveParams: { postId: "123" },
                spellcheck: true,
                plainPaste: true,
                imageButtons: ["removeImage", "replaceImage", "linkImage"],
                borderColor: '#00008b',
                imageUploadURL: '@Url.Action("FroalaUploadImage", "Home")',
                imageParams: { postId: "123" },
                enableScript: false
            });
        });
    </script>
}
اگر می‌خواهید فونت پیش فرض آن را تنظیم کنید، باید مطابق کدهای ابتدای فایل، ویژگی‌های froala-element را تغییر دهید.
سپس این ادیتور را بر روی المان TextArea قرار گرفته در صفحه، فعال می‌کنیم.
در قسمت مقادیر buttons، تمام حالات ممکن پیش بینی شده‌اند. هر کدام را که نیاز ندارید، حذف کنید.
نحوه‌ی تعریف زبان و راست به چپ بودن این ادیتور را با مقدار دهی پارامترهای language و direction ملاحظه می‌کنید.

پارامترهای autosave، saveURL و saveParams کار تنظیم ارسال خودکار محتوای ادیتور را جهت ذخیره‌ی آن در سرور به عهده دارند. بر اساس مقدار autosaveInterval می‌توان مشخص کرد که هر چند میلی ثانیه یکبار این‌کار باید انجام شود.
        /// <summary>
        /// ذخیره سازی خودکار
        /// </summary>
        [HttpPost]
        [ValidateInput(false)]
        public ActionResult FroalaAutoSave(string body, int? postId) // نام پارامتر بادی را تغییر ندهید
        {
            //todo: save body ...
            return new EmptyResult();
        }
در قسمت سمت سرور هم می‌توان این مقادیر ارسالی را در اکشن متدی که ملاحظه می‌کنید، دریافت کرد.
چون قرار است تگ‌های HTML به سرور ارسال شوند، ویژگی ValidateInput به false تنظیم شده‌است.
saveParams آن، برای مقدار دهی پارامترهای اضافی است که نیاز می‌باشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.
در اینجا نام پارامتری که ارسال می‌گردد، دقیقا مساوی body است. بنابراین آن‌را تغییر ندهید.

پارامترهای imageUploadURL و imageParams برای فعال سازی ذخیره تصاویر آن در سرور کاربرد دارند.
اکشن متد مدیریت کننده‌ی آن به نحو ذیل می‌تواند تعریف شود:
        // todo: مسایل امنیتی آپلود را فراموش نکنید
        /// <summary>
        /// ذخیره سازی تصاویر ارسالی
        /// </summary>
        [HttpPost]
        public ActionResult FroalaUploadImage(HttpPostedFileBase file, int? postId) // نام پارامتر فایل را تغییر ندهید
        {
            var fileName = Path.GetFileName(file.FileName);
            var rootPath = Server.MapPath("~/images/");
            file.SaveAs(Path.Combine(rootPath, fileName));
            return Json(new { link = "images/" + fileName }, JsonRequestBehavior.AllowGet);
        }
در اینجا نام پارامتری که به سرور ارسال می‌گردد، دقیقا معادل file است. بنابراین آن‌را تغییر ندهید.
خروجی آن برای مشخص سازی محل ذخیره سازی تصویر در سرور باید یک خروجی JSON دارای خاصیت و پارامتر link به نحو فوق باشد (این مسیر، یک مسیر نسبی است؛ نسبت به ریشه سایت).
imageParams آن برای مقدار دهی پارامترهای اضافی است که نیاز می‌باشند تا به سرور ارسال شوند. مثلا شماره مطلب جاری نیز به سرور ارسال گردد.


استفاده از Froala WYSIWYG Editor در ASP.NET Web forms
تمام نکاتی که در قسمت تنظیمات ASP.NET MVC در مورد ویژگی‌های سمت کلاینت این ادیتور ذکر شد، در مورد وب فرم‌ها نیز صادق است. فقط قسمت مدیریت سمت سرور آن اندکی تفاوت دارد.
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master"
    ValidateRequest="false"
    EnableEventValidation="false"
    AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="FroalaWebFormsTest.Default" %>

<%--اعتبارسنجی ورودی غیرفعال شده چون باید تگ ارسال شود--%>
<%--همچنین در وب کانفیگ هم تنظیم دیگری نیاز دارد--%>

<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
    <%--حالت کلاینت آی دی بهتر است تنظیم شود در اینجا--%>
    <asp:TextBox ID="txtEditor" ClientIDMode="Static"
        runat="server" Height="199px" TextMode="MultiLine" Width="447px"></asp:TextBox>
    <br />
    <asp:Button ID="btnSave" runat="server" OnClick="btnSave_Click" Text="ارسال" />

    <style type="text/css">
        /*تنظیم فونت پیش فرض ادیتور*/
        .froala-element {
        }
    </style>

    <script type="text/javascript">
        $(function () {
            $('#txtEditor').editable({
                buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily",
                    "fontSize", "color", "formatBlock", "align", "insertOrderedList",
                    "insertUnorderedList", "outdent", "indent", "selectAll", "createLink",
                    "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"],
                inlineMode: false,
                inverseSkin: true,
                preloaderSrc: 'Content/img/preloader.gif',
                allowedImageTypes: ["jpeg", "jpg", "png"],
                height: 300,
                language: "fa",
                direction: "rtl",
                fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"],
                autosave: true,
                autosaveInterval: 2500,
                saveURL: 'FroalaHandler.ashx',
                saveParams: { postId: "123" },
                spellcheck: true,
                plainPaste: true,
                imageButtons: ["removeImage", "replaceImage", "linkImage"],
                borderColor: '#00008b',
                imageUploadURL: 'FroalaHandler.ashx',
                imageParams: { postId: "123" },
                enableScript: false
            });
        });
    </script>
</asp:Content>
همانطور که ملاحظه می‌کنید،  ValidateRequest صفحه به false تنظیم شده و همچنین در وب کانفیگ httpRuntime requestValidationMode به نگارش 2 تنظیم گردیده‌است تا بتوان توسط این ادیتور تگ‌های ارسالی را به سرور ارسال کرد.
به علاوه ClientIDMode=Static نیز تنظیم شده‌است، تا بتوان از ID تکست باکس قرار گرفته در صفحه، به سادگی در کدهای سمت کاربر جی‌کوئری استفاده کرد.
اگر دقت کرده باشید، save urlها اینبار به فایل FroalaHandler.ashx اشاره می‌کنند. محتوای این Genric handler را ذیل مشاهده می‌کنید:
using System.IO;
using System.Web;
using System.Web.Script.Serialization;

namespace FroalaWebFormsTest
{
    public class FroalaHandler : IHttpHandler
    {
        //todo: برای اینکارها بهتر است از وب ای پی آی استفاده شود
        //todo: یا دو هندلر مجزا یکی برای تصاویر و دیگری برای ذخیره سازی متن

        public void ProcessRequest(HttpContext context)
        {
            var body = context.Request.Form["body"];
            var postId = context.Request.Form["postId"];
            if (!string.IsNullOrWhiteSpace(body) && !string.IsNullOrWhiteSpace(postId))
            {
                //todo: save changes

                context.Response.ContentType = "text/plain";
                context.Response.Write("");
                context.Response.End();
            }

            var files = context.Request.Files;
            if (files.Keys.Count > 0)
            {
                foreach (string fileKey in files)
                {
                    var file = context.Request.Files[fileKey];
                    if (file == null || file.ContentLength == 0)
                        continue;

                    //todo: در اینجا مسایل امنیتی آپلود فراموش نشود
                    var fileName = Path.GetFileName(file.FileName);
                    var rootPath = context.Server.MapPath("~/images/");
                    file.SaveAs(Path.Combine(rootPath, fileName));


                    var json = new JavaScriptSerializer().Serialize(new { link = "images/" + fileName });
                    // البته اینجا یک فایل بیشتر ارسال نمی‌شود
                    context.Response.ContentType = "text/plain";
                    context.Response.Write(json);
                    context.Response.End();
                }
            }

            context.Response.ContentType = "text/plain";
            context.Response.Write("");
            context.Response.End();
        }

        public bool IsReusable
        {
            get { return false; }
        }
    }
}
در اینجا نحوه‌ی مدیریت سمت سرور auto save و همچنین ارسال تصاویر ادیتور Froala ، ذکر شده‌اند. با استفاده از context.Request.Form می‌توان به عناصر ارسالی به سرور دسترسی پیدا کرد. همچنین توسط context.Request.Files، اگر فایلی ارسال شده بود، ذخیره شده و نهایتا خروجی JSON مدنظر بازگشت داده می‌شود.


یک نکته‌ی امنیتی مهم
<location path="upload">
  <system.webServer>
    <handlers accessPolicy="Read" />
  </system.webServer>
</location>
تنظیم فوق را در web.config سایت، جهت Read only کردن پوشه‌ی ارسال تصاویر، حتما مدنظر داشته باشید. در اینجا فرض شده‌است که پوشه‌ی uploads قرار است قابلیت اجرای فایل‌های پویا را نداشته باشد.


کدهای کامل این مطلب را در ادامه می‌توانید دریافت کنید
Froala-Sample
 
نظرات مطالب
بدست آوردن برگهای یک درخت توسط Recursive CTE
من دقیقا متوجه نشدم نتیجه مورد نظر شما چیست.
آیا نتیجه مورد نظر شما به صورت الحاق یافته (concatenated)  هست یا نه؟
در هر صورت باید یکی از دو query زیر نتیجه مورد نظر شما را تولید کند.
declare @t table
(id char(1) primary key,
parent char(1));
 
insert @t values
('A',null),                                   --Level 1
('B', 'A'), ('C', 'A'),                       --Level 2
('D', 'B'), ('E', 'B'),('R','B'), ('F', 'C'), --Level 3
('G', 'D'),                                   --Level 4
('H', 'G'), ('I', 'G');                       --Level 5

;with cte as
(
select id, rnk=0, 
       concats = cast(id as varchar(10))
from @t
where parent is null
 
union all
 
select t.id, rnk+1,
       cast(cte.concats + t.id as varchar(10))
from cte join @t t
on cte.id = t.parent
)
select * from cte
/*
id   rnk         concats
---- ----------- ----------
A    0           A
B    1           AB
C    1           AC
F    2           ACF
D    2           ABD
E    2           ABE
R    2           ABR
G    3           ABDG
H    4           ABDGH
I    4           ABDGI
*/
;with cte as
(
select id, rnk=0, 
       concats = cast(id as varchar(10))
from @t
where parent is null
 
union all
 
select t.id, rnk+1,
       cast(cte.concats + t.id as varchar(10))
from cte join @t t
on cte.id = t.parent
)
select stuff(d.list,1,1,'') as concats
from (select ','+concats
      from cte
  for xml path(''))d(list)
/*
concats
----------------------------------------
A,AB,AC,ACF,ABD,ABE,ABR,ABDG,ABDGH,ABDGI
*/

موفق باشید
مطالب دوره‌ها
بررسی جزئیات تزریق وابستگی‌ها در قالب پروژه WPF Framework
در قالب طراحی شده، نه در کدهای Viewهای اضافه شده و نه در ViewModelها، اثری از کدهای مرتبط با تزریق وابستگی‌ها و یا حتی وهله سازی ViewModel مرتبط با یک View مشاهده نمی‌شود. در ادامه قصد داریم جزئیات پیاده سازی آن‌را مرور کنیم.

مدیریت خودکار وهله سازی ViewModelها

اگر به فایل MVVM\ViewModelFactory.cs قرار گرفته در پروژه Common مراجعه کنید، کدهای کلاسی که کار وهله سازی ViewModelها را انجام می‌دهد، مشاهده خواهید کرد:
using System.Windows;
using StructureMap;

namespace WpfFramework1999.Common.MVVM
{
    /// <summary>
    /// Stitches together a view and its view-model
    /// </summary>
    public class ViewModelFactory
    {
        private readonly FrameworkElement _control;

        /// <summary>
        /// سازنده کلاس تزریق وابستگی‌ها به ویوو مدل و وهله سازی آن
        /// </summary>
        /// <param name="control">وهله‌ای از شیءایی که باید کار تزریق وابستگی‌ها در آن انجام شود</param>
        public ViewModelFactory(FrameworkElement control)
        {
            _control = control;
        }

        /// <summary>
        /// وهله متناظر با ویوو مدل
        /// </summary>
        public IViewModel ViewModelInstance { get; private set; }

        /// <summary>
        /// کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        /// </summary>        
        public void WireUp()
        {
            var viewName = _control.GetType().Name;
            var viewModelName = string.Concat(viewName, "ViewModel"); //قرار داد نامگذاری ما است

            if (!_control.IsLoaded)
            {
                _control.Loaded += (s, e) =>
                {
                    setDataContext(viewModelName);
                };
            }
            else
            {
                setDataContext(viewModelName);
            }
        }

        private void setDataContext(string viewModelName)
        {
            //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
            ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);
            if (ViewModelInstance == null) // این صفحه ویوو مدل ندارد
                return;

            _control.DataContext = ViewModelInstance;
        }
    }
}
در این کلاس، یک وهله از صفحه‌ای که توسط کاربر درخواست شده‌است، در سازنده کلاس دریافت گردیده و سپس در متد WireUp، بر اساس قرارداد نامگذاری که پیشتر نیز عنوان شد، ViewModel متناظر با نام View از IoC Container استخراج و وهله سازی می‌گردد. سپس این وهله به DataContext صفحه انتساب داده می‌شود.
چند سؤال مهم:
- IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟
- این کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟


IoC Container از کجا می‌داند که ViewModelها در کجا قرار دارند؟

اگر بحث سری جاری را از ابتدا دنبال کرده باشید، عنوان شد که ViewModelها را در این قالب، باید مشتق شده از کلاس پایه‌ای به نام BaseViewModel تهیه کنیم. برای مثال:
/// <summary>
/// ویوو مدل افزودن و مدیریت کاربران
/// </summary>
public class AddNewUserViewModel : BaseViewModel
این کلاس پایه که در فایل MVVM\BaseViewModel.cs پروژه Common قرار دارد، به نحو زیر آغاز شده است:
/// <summary>
/// کلاس پایه ویوو مدل‌های برنامه که جهت علامتگذاری آن‌ها برای سیم کشی‌های تزریق وابستگی‌های برنامه نیز استفاده می‌شود
/// </summary>
public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
اگر دقت کنید در اینجا اینترفیس IViewModel نیز ذکر شده است. این اینترفیس برای علامتگذاری ViewModelها و یافتن خودکار آن‌ها توسط IoC Container مورد استفاده درنظر گرفته شده است. اگر به فایل Core\IocConfig.cs پروژه Infrastructure مراجعه کنید، چنین تنظیمی را در آن مشاهده خواهید نمود:
// Add all types that implement IView into the container,
// and name each specific type by the short type name.
scan.AddAllTypesOf<IViewModel>().NameBy(type => type.Name);
به این ترتیب StructureMap با اسکن اسمبلی Infrastructure کلیه کلاس‌های پیاده سازی کننده IViewModel را یافته و سپس آن‌ها را بر اساس نام متناظری که دارند، ذخیره می‌کند. با این تنظیم، اکنون در کلاس ViewModelFactory یک چنین کدی کار خواهد کرد:
 //کار تزریق خودکار وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);


کلاس ViewModelFactory چگونه به وهله‌ای از یک صفحه درخواستی توسط کاربر دسترسی پیدا می‌کند و در کجا؟

در اینجا قسمتی از کدهای فایل Core\FrameFactory.cs قرار گرفته در پروژه Infrastructure را ملاحظه می‌کنید:
namespace WpfFramework.Infrastructure.Core
{
    /// <summary>
    /// ایجاد یک کنترل فریم سفارشی که قابلیت تزریق وابستگی‌ها را به صورت خودکار دارد
    /// به همراه اعمال مسایل راهبری برنامه که از منوی اصلی دریافت می‌شوند
    /// </summary>
    public class FrameFactory : Frame
    {
        /// <summary>
        /// در اینجا می‌شود به وهله‌ای از صفحه‌ای که قرار است اضافه گردد دسترسی یافت
        /// </summary>
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            var newPage = newContent as FrameworkElement;
            if (newPage == null)
                return;

            _currentViewModelFactory = new ViewModelFactory(newPage);
            _currentViewModelFactory.WireUp(); //کار تزریق وابستگی‌ها و وهله سازی ویوو مدل مرتبط انجام خواهد شد
        }
    }
}
در این کلاس، یک Frame سفارشی را طراحی کرده‌ایم؛ از این جهت که بتوان متد OnContentChanged آن‌را تحریف کرد. در این متد، newContent دقیقا وهله‌ای از صفحه جدیدی است که توسط کاربر درخواست شده‌است. خوب ... این وهله را داریم، بنابراین تنها کافی است آن‌را به کلاس ViewModelFactory ارسال کنیم و متد WireUp آن‌را بر روی وهله کلاس صفحه درخواستی فراخوانی نمائیم. به این ترتیب، صفحه‌ای نمایش داده خواهد شد که DataContext آن با وهله‌ای از ViewModel متناظر مقدار دهی شده‌است. از این جهت که این وهله سازی توسط IoC Container صورت می‌گیرد، کلیه وابستگی‌های تعریف شده در سازنده کلاس ViewModel نیز به صورت خودکار وهله سازی و مقدار دهی خواهند شد.

نهایتا فراخوانی متد IocConfig.Init، در فایل App.xaml.cs پروژه ریشه، در آغاز برنامه قرار گرفته است.
مطالب
تبدیل عدد به حروف

به طور قطع توابع و کلاس‌های تبدیل عدد به حروف، در جعبه ابزار توابع کمکی شما هم پیدا می‌شوند. روز قبل سعی کردم جهت آزمایش، عدد 3000,000,000,000,000 ریال را با کلاسی که دارم تست کنم و نتیجه overflow یا اصطلاحا ترکیدن سیستم بود! البته اگر مطالب این سایت را دنبال کرده باشید پیشتر در همین راستا مطلبی در مورد نحوه‌ی صحیح بکارگیری توابع تجمعی SQL در این سایت منتشر شده است و جزو الزامات هر سیستمی است (تفاوتی هم نمی‌کند که به چه زبانی تهیه شده باشد). اگر آ‌ن‌را رعایت نکرده‌اید، سیستم شما «روزی» دچار overflow خواهد شد.

در کل این کلاس تبدیل عدد به حروف را به صورت ذیل اصلاح کردم و همچنین دو زبانه است؛ چیزی که کمتر در پیاده سازی‌های عمومی به آن توجه شده است:

using System.Collections.Generic;
using System.Linq;

namespace NumberToWordsLib
{
/// <summary>
/// Number to word languages
/// </summary>
public enum Language
{
/// <summary>
/// English Language
/// </summary>
English,

/// <summary>
/// Persian Language
/// </summary>
Persian
}

/// <summary>
/// Digit's groups
/// </summary>
public enum DigitGroup
{
/// <summary>
/// Ones group
/// </summary>
Ones,

/// <summary>
/// Teens group
/// </summary>
Teens,

/// <summary>
/// Tens group
/// </summary>
Tens,

/// <summary>
/// Hundreds group
/// </summary>
Hundreds,

/// <summary>
/// Thousands group
/// </summary>
Thousands
}

/// <summary>
/// Equivalent names of a group
/// </summary>
public class NumberWord
{
/// <summary>
/// Digit's group
/// </summary>
public DigitGroup Group { set; get; }

/// <summary>
/// Number to word language
/// </summary>
public Language Language { set; get; }

/// <summary>
/// Equivalent names
/// </summary>
public IList<string> Names { set; get; }
}

/// <summary>
/// Convert a number into words
/// </summary>
public static class HumanReadableInteger
{
#region Fields (4)

private static readonly IDictionary<Language, string> And = new Dictionary<Language, string>
{
{ Language.English, " " },
{ Language.Persian, " و " }
};
private static readonly IList<NumberWord> NumberWords = new List<NumberWord>
{
new NumberWord { Group= DigitGroup.Ones, Language= Language.English, Names=
new List<string> { string.Empty, "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }},
new NumberWord { Group= DigitGroup.Ones, Language= Language.Persian, Names=
new List<string> { string.Empty, "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" }},

new NumberWord { Group= DigitGroup.Teens, Language= Language.English, Names=
new List<string> { "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" }},
new NumberWord { Group= DigitGroup.Teens, Language= Language.Persian, Names=
new List<string> { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" }},

new NumberWord { Group= DigitGroup.Tens, Language= Language.English, Names=
new List<string> { "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }},
new NumberWord { Group= DigitGroup.Tens, Language= Language.Persian, Names=
new List<string> { "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" }},

new NumberWord { Group= DigitGroup.Hundreds, Language= Language.English, Names=
new List<string> {string.Empty, "One Hundred", "Two Hundred", "Three Hundred", "Four Hundred",
"Five Hundred", "Six Hundred", "Seven Hundred", "Eight Hundred", "Nine Hundred" }},
new NumberWord { Group= DigitGroup.Hundreds, Language= Language.Persian, Names=
new List<string> {string.Empty, "یکصد", "دویست", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد" , "نهصد" }},

new NumberWord { Group= DigitGroup.Thousands, Language= Language.English, Names=
new List<string> { string.Empty, " Thousand", " Million", " Billion"," Trillion", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
new NumberWord { Group= DigitGroup.Thousands, Language= Language.Persian, Names=
new List<string> { string.Empty, " هزار", " میلیون", " میلیارد"," تریلیون", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
};
private static readonly IDictionary<Language, string> Negative = new Dictionary<Language, string>
{
{ Language.English, "Negative " },
{ Language.Persian, "منهای " }
};
private static readonly IDictionary<Language, string> Zero = new Dictionary<Language, string>
{
{ Language.English, "Zero" },
{ Language.Persian, "صفر" }
};

#endregion Fields

#region Methods (7)

// Public Methods (5) 

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this int number, Language language)
{
return NumberToText((long)number, language);
}


/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this uint number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this byte number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this decimal number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this double number, Language language)
{
return NumberToText((long)number, language);
}

/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this long number, Language language)
{
if (number == 0)
{
return Zero[language];
}

if (number < 0)
{
return Negative[language] + NumberToText(-number, language);
}

return wordify(number, language, string.Empty, 0);
}
// Private Methods (2) 

private static string getName(int idx, Language language, DigitGroup group)
{
return NumberWords.Where(x => x.Group == group && x.Language == language).First().Names[idx];
}

private static string wordify(long number, Language language, string leftDigitsText, int thousands)
{
if (number == 0)
{
return leftDigitsText;
}

var wordValue = leftDigitsText;
if (wordValue.Length > 0)
{
wordValue += And[language];
}

if (number < 10)
{
wordValue += getName((int)number, language, DigitGroup.Ones);
}
else if (number < 20)
{
wordValue += getName((int)(number - 10), language, DigitGroup.Teens);
}
else if (number < 100)
{
wordValue += wordify(number % 10, language, getName((int)(number / 10 - 2), language, DigitGroup.Tens), 0);
}
else if (number < 1000)
{
wordValue += wordify(number % 100, language, getName((int)(number / 100), language, DigitGroup.Hundreds), 0);
}
else
{
wordValue += wordify(number % 1000, language, wordify(number / 1000, language, string.Empty, thousands + 1), 0);
}

if (number % 1000 == 0) return wordValue;
return wordValue + getName(thousands, language, DigitGroup.Thousands);
}

#endregion Methods
}
}



دریافت پروژه کامل به همراه Unit tests مرتبط


اشتراک‌ها
پیاده سازی یک Filter سفارشی برای نگاشت استثنای همزمانی به خطاهای MadelState
 public class HandleConcurrencyExceptionAttribute : FilterAttribute, IExceptionFilter
    {
        private PropertyMatchingMode _propertyMatchingMode;
        /// <summary>
        /// This defines when the concurrencyexception happens, 
        /// </summary>
        public enum PropertyMatchingMode
        {
            /// <summary>
            /// Uses only the field names in the model to check against the entity. This option is best when you are using 
            /// View Models with limited fields as opposed to an entity that has many fields. The ViewModel (or model) field names will
            /// be used to check current posted values vs. db values on the entity itself.
            /// </summary>
            UseViewModelNamesToCheckEntity = 0,
            /// <summary>
            /// Use any non-matching value fields on the entity (except timestamp fields) to add errors to the ModelState.
            /// </summary>
            UseEntityFieldsOnly = 1,
            /// <summary>
            /// Tells the filter to not attempt to add field differences to the model state.
            /// This means the end user will not see the specifics of which fields caused issues
            /// </summary>
            DontDisplayFieldClashes = 2
        }


        public HandleConcurrencyExceptionAttribute()
        {
            _propertyMatchingMode = PropertyMatchingMode.UseViewModelNamesToCheckEntity;
        }

        public HandleConcurrencyExceptionAttribute(PropertyMatchingMode propertyMatchingMode)
        {
            _propertyMatchingMode = propertyMatchingMode;
        }


        /// <summary>
        /// The main method, called by the mvc runtime when an exception has occured.
        /// This must be added as a global filter, or as an attribute on a class or action method.
        /// </summary>
        /// <param name="filterContext"></param>
        public void OnException(ExceptionContext filterContext)
        {
            if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException)
            {
                //Get original and current entity values
                DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception;
                var entry = ex.Entries.Single();
                //problems with ef4.1/4.2 here because of context/model in different projects.
                //var databaseValues = entry.CurrentValues.Clone().ToObject();
                //var clientValues = entry.Entity;
                //So - if using EF 4.1/4.2 you may use this workaround
                var clientValues = entry.CurrentValues.Clone().ToObject();
                entry.Reload();
                var databaseValues = entry.CurrentValues.ToObject();

                List<string> propertyNames;

                filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again to cause your changes to be the current saved values.");
                PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);

                if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity)
                {
                    //We dont have access to the model here on an exception. Get the field names from modelstate:
                    propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList();
                }
                else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly)
                {
                    propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList();
                }
                else
                {
                    filterContext.ExceptionHandled = true;
                    UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
                    filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
                    return;
                }



                UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);

                //Get all public properties of the entity that have names matching those in our modelstate.
                foreach (var propertyInfo in entityFromDbProperties)
                {

                    //If this value is not in the ModelState values, don't compare it as we don't want
                    //to attempt to emit model errors for fields that don't exist.

                    //Compare db value to the current value from the entity we posted.

                    if (propertyNames.Contains(propertyInfo.Name))
                    {
                        if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
                        {
                            var currentValue = propertyInfo.GetValue(databaseValues, null);
                            if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
                            {
                                currentValue = "Empty";
                            }

                            filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Current value: "
                                 + currentValue);
                        }
                    }

                    //TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved?
                    //The problem here is we may only have a few fields used in the viewmodel, but many in the entity
                    //so we could have a problem here with that.
                    //object o = propertyInfo.GetValue(myObject, null);
                }

                filterContext.ExceptionHandled = true;

                filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
            }
        }
پیاده سازی یک Filter سفارشی برای نگاشت استثنای همزمانی به خطاهای MadelState
مطالب
Static Reflection

قابلیت Dynamic reflection یا به اختصار همان reflection متداول، از اولین نگارش‌های دات نت فریم در دسترس است و امکان دسترسی به اطلاعات مرتبط با کلاس‌ها، متدها، خواص و غیره را در زمان اجرا مهیا می‌سازد. تابحال به کمک این قابلیت، امکان تهیه‌ی ابزارهای پیشرفته‌ی زیر مهیا شده است:
انواع و اقسام
- فریم ورک‌های آزمون واحد
- code generators
- ORMs
- ابزارهای آنالیز کد
و ...


برای مثال فرض کنید که می‌خواهید برای یک کلاس به صورت خودکار، متدهای آزمون واحد تهیه کنید (تهیه یک code generator ساده). اولین نیاز این برنامه، دسترسی به امضای متدها به همراه نام آرگومان‌ها و نوع آن‌ها است. برای حل این مساله باید برای مثال یک parser زبان سی شارپ یا اگر بخواهید کامل‌تر کار کنید، به ازای تمام زبان‌های قابل استفاده در دات نت فریم ورک باید parser تهیه کنید که ... کار ساده‌ای نیست. اما با وجود reflection به سادگی می‌توان به این نوع اطلاعات دسترسی پیدا کرد و نکته‌ی مهم آن هم این است که مستقل است از نوع زبان مورد استفاده. به همین جهت است که این نوع ابزارها را در فریم ورک‌هایی که فاقد امکانات reflection هستند، کمتر می‌توان یافت. برای مثال کیفیت کتابخانه‌های آزمون واحد CPP در مقایسه با آنچه که در دات نت مهیا هستند، اصلا قابل مقایسه نیستند. برای نمونه به یکی از معظم‌ترین فریم ورک‌های آزمون واحد CPP که توسط گوگل تهیه شده مراجعه کنید : (+)
قابلیت Reflection ، مطلب جدیدی نیست و برای مثال زبان جاوا هم سال‌ها است که از آن‌ پشتیبانی می‌کند. اما نگارش سوم دات نت فریم ورک با معرفی lambda expressions ، LINQ و Expressions در یک سطح بالاتر از این Dynamic reflection متداول قرار گرفت.

تعریف Static Reflection :
استفاده از امکانات Reflection API بدون بکارگیری رشته‌ها، به کمک قابلیت اجرای به تعویق افتاده‌ی LINQ، جهت دسترسی به متادیتای المان‌های کد، مانند خواص، متدها و غیره.
برای مثال کد زیر را در نظر بگیرید:
//dynamic reflection
PropertyInfo property = typeof (MyClass).GetProperty("Name");
MethodInfo method = typeof (MyClass).GetMethod("SomeMethod");
این کد، یک نمونه از دسترسی به متادیتای خواص یا متدها را به کمک Reflection متداول نمایش می‌دهد. مهم‌ترین ایراد آن استفاده از رشته‌ها است که تحت نظر کامپایلر نیستند و تنها زمان اجرا است که مشخص می‌شود آیا MyClass واقعا خاصیتی به نام Name داشته است یا خیر.
چقدر خوب می‌شد اگر این قابلیت بجای dynamic بودن (مشخص شدن در زمان اجرا)، استاتیک می‌بود و در زمان کامپایل قابل بررسی می‌شد. این امکان به کمک lambda expressions و expression trees دات نت سه بعد، میسر شده است. کلیدهای اصلی Static Reflection کلاس‌های Func و Expression هستند. با استفاده از کلاس Func می‌توان lambda expression ایی را تعریف کرد که مقداری را بر می‌گرداند و توسط کلاس Expression می‌توان به محتوای یک delegate دسترسی یافت. ترکیب این دو، قدرت دستیابی به اطلاعاتی مانند PropertyInfo را در زمان طراحی کلاس‌ها، می‌دهد؛ با توجه به اینکه:
- کاملا توسط intellisense موجود در VS.NET پشتیبانی می‌شود.
- با استفاده از ابزارهای refactoring قابل کنترل است.
- از همه مهم‌تر، دیگری خبری از رشته‌ها نبوده و همه چیز تحت کنترل کامپایلر قرار می‌گیرد.

و شاید هیچ قابلیتی به اندازه‌ی Static Reflection در این چندسال اخیر بر روی اکوسیستم دات نت فریم ورک تاثیرگذار نبوده باشد. این روزها کمتر کتابخانه یا فریم ورکی را می‌توانید پیدا کنید که از Static Reflection استفاده نکند. سرآغاز استفاده گسترده از آن به Fluent NHibernate بر می‌گردد؛ سپس در انواع و اقسام mocking frameworks‌ ، ORMs و غیره استفاده شد و مدتی است که در ASP.NET MVC نیز مورد استفاده قرار می‌گیرد (برای مثال TextBoxFor معروف آن):
public string TextBoxFor<T>(Expression<Func<T,object>> expression);
به این ترتیب حین استفاده از آن دیگری نیازی نخواهد بود تا نام خاصیت مدل مورد نظر را به صورت رشته وارد کرد:
<%= this.TextBoxFor(model => model.FirstName); %>

یک مثال ساده از تعریف و بکارگیری Static Reflection :
public PropertyInfo GetProperty<T>(Expression<Func<T, object>> expression)
{
var memberExpression = expression.Body as MemberExpression;

if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");

return memberExpression.Member as PropertyInfo;
}
همانطور که عنوان شد کلیدهای اصلی بهر‌ه‌گیری از امکانات Static reflection ، استفاده از کلاس‌های Expression و Func هستند که در آرگومان متد فوق بکارگرفته شده‌اند و در حقیقت یک expression of a delegate است که به آن Lambdas as Data نیز گفته می‌شود. این delegate پارامتری از نوع T را دریافت کرده و سپس مقداری از نوع object را بر می‌گرداند. اما زمانیکه از کلاس Expression در اینجا استفاده می‌شود، این Func دیگر اجرا نخواهد شد، بلکه از آن به عنوان قطعه‌ کدی که اطلاعاتش قرار است استخراج شود (Lambdas as Data) استفاده می‌شود.
برای نمونه Fluent NHibernate‌ در پشت صحنه متد Map ، به کمک متدی شبیه به GetProperty فوق، a => a.Address1 را به رشته متناظر خاصیت Address1 تبدیل کرده و جهت تعریف نگاشت‌ها مورد استفاده قرار می‌دهد:
public class AddressMap : DomainMap<Address>
{
public AddressMap()
{
Map(a => a.Address1);
}
}

جهت اطلاع؛ قابلیت استفاده از «کد به عنوان اطلاعات» هم مفهوم جدیدی نیست و برای مثال زبان Lisp چند دهه است که آن‌را ارائه داده است!

برای مطالعه بیشتر:

اشتراک‌ها
دات نت پایه، سی شارپ 8.0 و Nullable Reference Types

Here are some of the reasons why nullable reference types are less than ideal:

  • Invoking a member on a null value will issue a System.NullReferenceException exception, and every invocation that results in a System.NullReferenceException in production code is a bug. Unfortunately, however, with nullable reference types we “fall in” to doing the wrong thing rather than the right thing. The “fall in” action is to invoke a reference type without checking for null.
  • There’s an inconsistency between reference types and value types (following the introduction of Nullable<T>) in that value types are nullable when decorated with  “?” (for example, int? number); otherwise, they default to non-nullable. In contrast, reference types are nullable by default. This is “normal” to those of us who have been programming in C# for a long time, but if we could do it all over, we’d want the default for reference types to be non-nullable and the addition of a “?” to be an explicit way to allow nulls.
  • It’s not possible to run static flow analysis to check all paths regarding whether a value will be null before dereferencing it, or not. Consider, for example, if there were unmanaged code invocations, multi-threading, or null assignment/­replacement based on runtime conditions. (Not to mention whether analysis would include checking of all library APIs that are invoked.)
  • There’s no reasonable syntax to indicate that a reference type value of null is invalid for a particular declaration.
  • There’s no way to decorate parameters to not allow null. 
دات نت پایه، سی شارپ 8.0 و Nullable Reference Types