مطالب
بررسی معماری پیازی - قسمت اول

 معماری پیازی توسط جفری پالرمو در سال 2008 ابداع شد. این معماری راه بهتری را برای ساخت برنامه‌های کاربردی جهت تست پذیری، نگهداری و قابلیت اطمینان بهتر بر روی زیرساخت‌هایی مانند پایگاه‌های داده و خدمات ارائه می‌دهد. هدف اصلی این معماری، پرداختن به چالش‌های پیش روی معماری 3 لایه و ارائه راه حلی برای مشکلات رایج مانند اتصال و جداسازی وابستگی‌ها است. دو نوع اتصال وجود دارند؛ اتصال محکم و اتصال ضعیف که در ادامه آنها را بررسی میکنیم.


اتصال محکم

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


اتصال ضعیف

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


مزایای معماری پیازی

چندین مزیت برای معماری پیازی وجود دارند که در زیر ذکر شده‌اند:

  • قابلیت نگهداری بهتری را فراهم می‌کند؛ زیرا همه کدها به لایه‌ها یا مرکز، بستگی دارند.
  • تست پذیری بهتری را فراهم می‌کند؛ زیرا آزمون واحد را می‌توان برای لایه‌های جداگانه، بدون تأثیر بر سایر ماژول‌های برنامه ایجاد کرد.
  • این برنامه یک برنامه‌ی کاربردی با اتصال آزاد را ایجاد می‌کند؛ زیرا لایه بیرونی برنامه، همیشه از طریق واسط‌ها با لایه داخلی، ارتباط برقرار می‌کند.
  • هرگونه پیاده سازی پیوسته، در زمان اجرا به برنامه ارائه می‌شود.
  • موجودیت‌های دامنه، هسته و بخش مرکزی هستند. می‌تواند به هر دو لایه پایگاه داده و UI دسترسی داشته باشد.
  • لایه‌های داخلی هرگز به لایه خارجی وابسته نیستند. کدی که ممکن است تغییر کرده باشد، باید بخشی از یک لایه خارجی باشد.


لایه‌های معماری پیاز

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


Domain Entities Layer

این بخش مرکزی معماری است. تمام اشیاء دامنه‌ی برنامه را در خود نگه می‌دارد. اگر برنامه ای با چهارچوب موجودیت ORM توسعه داده شود، این لایه دارای کلاس‌های POCO (Code First) یا Edmx (Database First) با موجودیت‌ها است. این نهادهای دامنه هیچ وابستگی ندارند.


Repository Layer

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


Service Layer

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


UI Layer

خارجی‌ترین لایه است و می‌تواند برنامه‌ی وب، Web API یا پروژه واحد تست باشد. این لایه دارای یک پیاده سازی از جنس Dependency Inversion Principle است بطوری که برنامه، یک برنامه‌ی کاربردی جفت شده‌ی آزاد می‌سازد و از طریق واسط‌ها با لایه داخلی ارتباط برقرار می‌کند.

در مطالب بعدی با مبحث معماری پیازی، نکات تکمیلی و مهمتری از لایه پیاز را تشریح میکنیم و یک پروژه را با معماری پیاز، راه اندازی میکنیم.

نظرات مطالب
راهبری در Silverlight به کمک الگوی MVVM
این هم خوبه؛ شبیه به ارسال اطلاعات به کمک یک delegate یا event . از سر ناچاری!
- ضمنا MVVM Light toolkit‌ سورس باز است. کلاس Messenger آن‌را جدا کنید و استفاده کنید (اگر از کل آن نمی‌خواهید استفاده کنید).

یک روش هم اینجا دیدم که خیلی جالب است:
http://forums.silverlight.net/forums/p/198684/463126.aspx
از NavigateUri یک HyperlinkButton استفاده کرده. فقط UriMapper را هم تنظیم کرده برای زیبایی کار.
(نیاز به هیچ کتابخانه جانبی هم ندارد. نیازی به دخالت MVVM هم ندارد.و مهم‌تر از همه، نیازی به کد نویسی هم اصلا ندارد.)

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

یک دوره آموزشی رایگان XAML اخیرا از طرف مایکروسافت ارائه شده است که از طریق آدرس زیر قابل دسترسی است:


این کلینیک آموزشی شامل موارد زیر است:
  • Navigation Overview
  • Clinic Information
  • Introduction to XAML
  • Overview of XAML
  • Why XAML?
  • XAML Layouts
  • Module Summary
  • XAML and WPF In Action
  • XAML in a Browser
  • Using XAML and code-behind in Desktop Applications
  • Module Summary
  • Unique Features of XAML
  • Resources
  • Styles and ControlTemplates
  • Module Summary
  • Glossary

این ماژول به صورت آفلاین نیز قابل دریافت است (به حجم 44 مگابایت) اما پیش از آن باید برنامه offline player آن‌را نصب نمود و طبق روال معمول سایت مایکروسافت، بهتر است از IE جهت مرور این صفحات استفاده کرد.

مطالب
سفارشی سازی ASP.NET Core Identity - قسمت اول - موجودیت‌های پایه و DbContext برنامه
با به پایان رسیدن مرحله‌ی توسعه‌ی ASP.NET Identity 2.x مخصوص نگارش‌های ASP.NETایی که از Full .NET Framework استفاده می‌کنند، نگارش جدید آن صرفا بر پایه‌ی ASP.NET Core تهیه شده‌است و در طی یک سری، نحوه‌ی سفارشی سازی تقریبا تمام اجزای آن‌را بررسی خواهیم کرد. جهت سهولت پیگیری این سری، پروژه‌ی کامل سفارشی سازی شده‌ی ASP.NET Core Identity را از مخزن کد DNT Identity می‌توانید دریافت کنید.


پیشنیازهای اجرای پروژه‌ی DNT Identity

 - ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
 - همچنین بانک اطلاعاتی پایه‌ی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل می‌شود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشته‌ی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شده‌است و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
 - کاربر پیش فرض Admin سیستم و کلمه‌ی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده می‌شوند.
 - تنظیمات ایمیل پیش فرض برنامه به استفاده‌ی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شده‌است. بنابراین تمام ایمیل‌های برنامه را جهت آزمایش محلی می‌توانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیل‌ها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامه‌ی Outlook می‌توان مشاهده کرد.


سفارشی سازی کلید اصلی موجودیت‌های ASP.NET Core Identity

ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری ساده‌ی آن که از یک string به عنوان نوع کلید اصلی استفاده می‌کنند و سری دوم، حالت جنریک که در آن می‌توان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیت‌های تغییر یافته را در پوشه‌ی src\ASPNETCoreIdentitySample.Entities\Identity نیز می‌توانید مشاهده کنید و شامل موارد ذیل هستند:

جدول نقش‌های سیستم
    public class Role : IdentityRole<int, UserRole, RoleClaim>, IAuditableEntity
    {
        public Role()
        {
        }

        public Role(string name)
            : this()
        {
            Name = name;
        }

        public Role(string name, string description)
            : this(name)
        {
            Description = description;
        }

        public string Description { get; set; }
    }
کار با ارث بری از نگارش جنریک کلاس IdentityRole شروع می‌شود. این کلاس پایه، حاوی تعاریف اصلی فیلدهای جدول نقش‌های سیستم است که اولین آرگومان جنریک آن، نوع کلید اصلی جدول مرتبط را نیز مشخص می‌کند و در اینجا به int تنظیم شده‌است. همچنین یک اینترفیس جدید IAuditableEntity را نیز در انتهای این تعریف‌ها مشاهده می‌کنید. در مورد این اینترفیس و Shadow properties متناظر با آن، در ادامه‌ی بحث با سفارشی سازی DbContext برنامه بیشتر توضیح داده خواهد شد.


در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.

جدول کاربران منتسب به نقش‌ها
    public class UserRole : IdentityUserRole<int>, IAuditableEntity
    {
        public virtual User User { get; set; }

        public virtual Role Role { get; set; }
    }
کلاس پایه‌ی جدول کاربران منتسب به نقش‌ها، کلاس جنریک IdentityUserRole است که در اینجا با تغییر آرگومان جنریک آن به int، نوع فیلدهای UserId و RoleId آن به int تنظیم می‌شوند. در کلاس سفارشی سازی شده‌ی فوق، دو خاصیت اضافه‌تر User و Role نیز را مشاهده می‌کنید. مزیت تعریف آن‌ها، دسترسی ساده‌تر به اطلاعات کاربران و نقش‌ها توسط EF Core است.


در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده می‌شود.


جدول جدید  IdentityRoleClaim سیستم
public class RoleClaim : IdentityRoleClaim<int>, IAuditableEntity
{
   public virtual Role Role { get; set; }
}
در ASP.NET Core Identity، جدول جدیدی به نام RoleClaim نیز اضافه شده‌است. در این سری از آن برای پیاده سازی سطوح دسترسی پویای به صفحات استفاده خواهیم کرد. ابتدا یک سری نقش ثابت در جدول Roles ثبت خواهند شد. سپس تعدادی کاربر به هر نقش نسبت داده می‌شوند. اکنون می‌توان به هر نقش نیز تعدادی Claim را انتساب داد. برای مثال یک Claim سفارشی که شامل ID سفارشی area:controller:action باشد. به این ترتیب و با بررسی سفارشی آن می‌توان سطوح دسترسی پویا را نیز پیاده سازی کرد و مزیت آن این است که تمام این Claims به صورت خودکار به کوکی شخص نیز اضافه شده و دسترسی به اطلاعات آن بسیار سریع است و نیازی به مراجعه‌ی به بانک اطلاعاتی را ندارد.

جدول UserClaim سیستم
public class UserClaim : IdentityUserClaim<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}
می‌توان به هر کاربر یک سری Claim مخصوص را نیز انتساب داد. برای مثال مسیر عکس ذخیره شده‌ی او در سرور، چه موردی است و این اطلاعات به صورت خودکار به کوکی او نیز توسط ASP.NET Core Identity اضافه می‌شوند. البته ما در این سری روش دیگری را برای سفارشی سازی Claims عمومی کاربران بکار خواهیم گرفت (با سفارشی سازی کلاس ApplicationClaimsPrincipalFactory آن).

جداول توکن و لاگین‌های کاربران
public class UserToken : IdentityUserToken<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}

public class UserLogin : IdentityUserLogin<int>, IAuditableEntity
{
   public virtual User User { get; set; }
}
دراینجا نیز نحوه‌ی سفارشی سازی و تغییر جداول لاگین‌های کاربران و توکن‌های مرتبط با آن‌ها را مشاهده می‌کنید. این جداول بیشتر جهت دسترسی به حالت‌هایی مانند لاگین با حساب کاربری جی‌میل مورد استفاده قرار می‌گیرند و کاربرد پیش فرضی ندارند (اگر از تامین کننده‌های لاگین خارجی نمی‌خواهید استفاده کنید).

جدول کاربران سیستم
    public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>, IAuditableEntity
    {
        public User()
        {
            UserUsedPasswords = new HashSet<UserUsedPassword>();
            UserTokens = new HashSet<UserToken>();
        }

        [StringLength(450)]
        public string FirstName { get; set; }

        [StringLength(450)]
        public string LastName { get; set; }

        [NotMapped]
        public string DisplayName
        {
            get
            {
                var displayName = $"{FirstName} {LastName}";
                return string.IsNullOrWhiteSpace(displayName) ? UserName : displayName;
            }
        }

        [StringLength(450)]
        public string PhotoFileName { get; set; }

        public DateTimeOffset? BirthDate { get; set; }

        public DateTimeOffset? CreatedDateTime { get; set; }

        public DateTimeOffset? LastVisitDateTime { get; set; }

        public bool IsEmailPublic { get; set; }

        public string Location { set; get; }

        public bool IsActive { get; set; } = true;

        public virtual ICollection<UserUsedPassword> UserUsedPasswords { get; set; }

        public virtual ICollection<UserToken> UserTokens { get; set; }
    }

    public class UserUsedPassword : IAuditableEntity
    {
        public int Id { get; set; }

        public string HashedPassword { get; set; }

        public virtual User User { get; set; }
        public int UserId { get; set; }
    }
در اینجا علاوه بر نحوه‌ی تغییر نوع کلید اصلی جدول کاربران سیستم، نحوه‌ی افزودن خواص اضافه‌تری مانند نام، تاریخ تولد، مکان، تصویر و غیره را نیز مشاهده می‌کنید. به علاوه جدولی نیز جهت ثبت سابقه‌ی کلمات عبور هش شده‌ی کاربران نیز تدارک دیده شده‌است تا کاربران نتوانند از 5 کلمه‌ی عبور اخیر خود (تنظیم NotAllowedPreviouslyUsedPasswords در فایل appsettings.json) استفاده کنند.
فیلد IsActive نیز از این جهت اضافه شده‌است تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.


تعریف Shadow properties ثبت تغییرات رکوردها

در #C ارث‌بری چندگانه‌ی کلاس‌ها ممنوع است؛ مگر اینکه از اینترفیس‌ها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمی‌توان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافه‌تری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده می‌کنید. این اینترفیس کار علامت‌گذاری کلاس‌هایی را انجام می‌دهد که قصد داریم به آن‌ها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاس‌هایی از نوع IAuditableEntity اعمال می‌شود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیت‌های علامت‌گذاری شده اضافه می‌کند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.


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


سفارشی سازی DbContext برنامه

نحوه‌ی سفارشی سازی DbContext برنامه را در پوشه‌ی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه می‌کنید. پوشه‌ی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازی‌های لازم بر روی آن انجام شده‌است؛ شامل:
 - تغییر نوع کلید اصلی موجودیت‌ها به همراه معرفی موجودیت‌های تغییر یافته:
 public abstract class ApplicationDbContextBase :
  IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>,
  IUnitOfWork
ما در ابتدای بحث، برای مثال کلاس Role را سفارشی سازی کردیم. اما برنامه از وجود آن بی‌اطلاع است. با ارث بری از IdentityDbContext و ذکر این کلاس‌های سفارشی به همراه نوع int کلید اصلی مورد استفاده، کار معرفی موجودیت‌های سفارشی سازی شده انجام می‌شود.

 - اعمال متد BeforeSaveTriggers به تمام نگارش‌های مختلف SaveChanges
protected void BeforeSaveTriggers()
{
  ValidateEntities();
  SetShadowProperties();
  this.ApplyCorrectYeKe();
}
در اینجا پیش از ذخیره‌ی اطلاعات، ابتدا موجودیت‌ها اعتبارسنجی می‌شوند. سپس مقادیر Shadow properties تنظیم شده و دست آخر، ی و ک فارسی نیز به اطلاعات ثبت شده، جهت یک دست سازی اطلاعات سیستم، اعمال می‌شوند.

- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شده‌ی در فایل appsettings.json خوانده شده و اعمال می‌شوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده می‌کنید. کار آن استفاده‌ی از بانک اطلاعاتی درون حافظه‌ای، جهت انجام آزمون‌های واحد و یا استفاده‌ی از LocalDb و یا نگارش کامل SQL Server می‌باشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.

پس از تعریف این DbContext پایه‌ی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده می‌کنید. این کلاس ‍Context ایی است که در برنامه از آن استفاده می‌شود و از کلاس پایه ApplicationDbContextBase مشتق شده‌است:
 public class ApplicationDbContext : ApplicationDbContextBase
تعاریف موجودیت‌های جدید خود را به این کلاس اضافه کنید.
تنظیمات mapping آن‌ها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوه‌ی استفاده‌ی از آن را به‌خاطر داشته باشید:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Adds all of the ASP.NET Core Identity related mappings at once.
            builder.AddCustomIdentityMappings(SiteSettings.Value);

            // Custom application mappings


            // This should be placed here, at the end.
            builder.AddAuditableShadowProperties();
        }
ابتدا باید base.OnModelCreating را ذکر کنید. در غیراینصورت تمام سفارشی سازی‌های شما بازنویسی می‌شوند.
سپس متد AddCustomIdentityMappings ذکر شده‌است. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه می‌کند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شده‌اند.


در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایه‌ای که پیشتر در مورد آن‌ها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشت‌های سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.

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


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
آشنایی با آزمایش واحد (unit testing) در دات نت، قسمت 4

ادامه آشنایی با NUnit

اگر قسمت سوم را دنبال کرده باشید احتمالا از تعداد مراحلی که باید در خارج از IDE صورت گیرد گلایه خواهید کرد (کامپایل کن، اجرا کن، اتچ کن، باز کن، ذخیره کن، اجرا کن و ...). خوشبختانه افزونه ReSharper این مراحل را بسیار ساده و مجتمع کرده است.
این افزونه به صورت خودکار متدهای آزمایش واحد یک پروژه را تشخیص داده و آنها را با آیکون‌هایی ( Gutter icons ) متمایز مشخص می‌سازد (شکل زیر). پس از کلیک بر روی آن‌ها ، امکان اجرای آزمایش یا حتی دیباگ کردن سطر به سطر یک متد آزمایش واحد درون IDE‌ ویژوال استودیو وجود خواهد داشت.



برای نمونه پس از اجرای آزمایش واحد قسمت قبل، نتیجه حاصل مانند شکل زیر خواهد بود:



راه دیگر، استفاده از افزونه TestDriven.NET است که نحوه استفاده از آن‌را اینجا می‌توانید ملاحظه نمائید. به منوی جهنده کلیک راست بر روی یک صفحه، گزینه run tests را اضافه می‌کند و نتیجه حاصل را در پنجره output ویژوال استودیو نمایش می‌دهد.

ساختار کلی یک کلاس آزمایش واحد مبتنی بر NUnit framework :

using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;

namespace TestLibrary
{
[TestFixture]
public class Test2
{
[SetUp]
public void MyInit()
{
//کدی که در این قسمت قرار می‌گیرد پیش از اجرای هر متد تستی اجرا خواهد شد
}

[TearDown]
public void MyClean()
{
//کدی که در این قسمت قرار می‌گیرد پس از اجرای هر متد تستی اجرا خواهد شد
}

[TestFixtureSetUp]
public void MyTestFixtureSetUp()
{
// کدی که در اینجا قرار می‌گیرد در ابتدای بررسی آزمایش واحد و فقط یکبار اجرا می‌شود
}

[TestFixtureTearDown]
public void MyTestFixtureTearDown()
{
// کدهای این قسمت در پایان کار یک کلاس آزمایش واحد اجرا خواهند شد
}

[Test]
public void Test1()
{
//بدنه آزمایش واحد در اینجا قرار می‌گیرد
Assert.That(2, Is.EqualTo(2));
}
}
}

شبیه به روال‌های رخداد گردان load و close یک فرم، یک کلاس آزمایش واحد NUnit نیز دارای ویژگی‌های TestFixtureSetUp و TestFixtureTearDown است که در ابتدا و انتهای آزمایش واحد اجرا خواهند شد (برای درک بهتر موضوع و دنبال کردن نحوه‌ی اجرای این روال‌ها، داخل این توابع break point قرار دهید و با استفاده از ReSharper ، آزمایش را در حالت دیباگ آغاز کنید)، یا SetUp و TearDown که در زمان آغاز و پایان بررسی هر متد آزمایش واحدی فراخوانی می‌شوند.
همانطور که در قسمت قبل نیز ذکر شد، به امضاهای متدها و کلاس فوق دقت نمائید (عمومی ، void و بدون آرگومان ورودی).
بهتر است از ویژگی‌های SetUp و TearDown با دقت استفاده نمود. عموما هدف از این روال‌ها ایجاد یک شیء و تخریب و پاک سازی آن است. حال اینکه این روال‌ها قبل و پس از اجرای هر متد آزمایش واحدی فراخوانی می‌شوند. بنابراین به این موضوع دقت داشته باشید.
همچنین توصیه می‌شود که کلاس‌های آزمایش واحد را در اسمبلی دیگری مجزا از پروژه اصلی پیاده سازی کنید (برای مثال یک پروژه جدید از نوع class library)، زیرا این موارد مرتبط با بررسی کیفیت کدهای شما هستند که موضوع جداگانه‌ای نسبت به پروژه اصلی محسوب می‌گردد (نحوه پیاده سازی آن‌‌را در قسمت قبل ملاحظه نمودید). همچنین در یک پروژه تیمی این جدا سازی، مدیریت آزمایشات را ساده‌تر می‌سازد و بعلاوه سبب حجیم شدن بی‌مورد اسمبلی‌های اصلی محصول شما نیز نمی‌گردند.


ادامه دارد...

مطالب
لینک‌های هفته‌ی آخر دی

وبلاگ‌ها ، سایت‌ها و مقالات ایرانی (داخل و خارج از ایران)


Visual Studio


ASP. Net



طراحی و توسعه وب



PHP


اس‌کیوال سرور


سی شارپ


عمومی دات نت


ویندوز


مسایل اجتماعی و انسانی برنامه نویسی


متفرقه


مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت سوم - اعتبارسنجی سمت کلاینت
FluentValidation یک کتابخانه‌ی اعتبارسنجی اطلاعات سمت سرور است و راهکاری را برای اعتبارسنجی‌های سمت کلاینت ارائه نمی‌دهد؛ اما می‌تواند متادیتای مورد نیاز unobtrusive java script validation را تولید کند و به این ترتیب بسیاری از قواعد آن دقیقا همانند روش‌های توکار اعتبارسنجی ASP.NET Core در سمت کلاینت نیز کار می‌کنند؛ مانند:
• NotNull/NotEmpty
• Matches (regex)
• InclusiveBetween (range)
• CreditCard
• Email
• EqualTo (cross-property equality comparison)
• MaxLength
• MinLength
• Length

اما باید دقت داشت که سایر قابلیت‌های این کتابخانه، قابلیت ترجمه‌ی به قواعد unobtrusive java script validation ندارند؛ مانند:
- استفاده‌ی از شرط‌ها توسط متدهای When و یا Unless (برای مثال اگر خاصیت A دارای مقدار 5 بود، آنگاه ورود اطلاعات خاصیت B باید اجباری شود)
- تعریف قواعد اعتبارسنجی سفارشی، برای مثال توسط متد Must
- تعریف RuleSetها (برای مثال تعیین کنید که از یک مدل، فقط تعدادی از خواص آن اعتبارسنجی شوند و نه تمام آن‌ها)

در یک چنین حالت‌هایی باید کاربر اطلاعات کامل فرم را به سمت سرور ارسال کند و در post-back صورت گرفته، نتایج اعتبارسنجی را دریافت کند.


روش توسعه‌ی اعتبارسنجی سمت کلاینت کتابخانه‌ی FluentValidation جهت سازگاری آن با  unobtrusive java script validation

باتوجه به اینکه قواعد سفارشی کتابخانه‌ی FluentValidation، معادل unobtrusive java script validation ای ندارند، در ادامه مثالی را جهت تدارک آن‌ها بررسی می‌کنیم. برای این منظور، معادل مطلب «ایجاد ویژگی‌های اعتبارسنجی سفارشی در ASP.NET Core 3.1 به همراه اعتبارسنجی سمت کلاینت آن‌ها» را بر اساس ویژگی‌های کتابخانه‌ی FluentValidation پیاده سازی می‌کنیم.

1) ایجاد اعتبارسنج سمت سرور

می‌خواهیم اگر مقدار عددی خاصیتی بیشتر از مقدار عددی خاصیت دیگری بود، اعتبارسنجی آن با شکست مواجه شود. به همین منظور با پیاده سازی یک PropertyValidator سفارشی، LowerThanValidator را به صورت زیر تعریف می‌کنیم:
using System;
using FluentValidation;
using FluentValidation.Validators;

namespace FluentValidationSample.Models
{
    public class LowerThanValidator : PropertyValidator
    {
        public string DependentProperty { get; set; }

        public LowerThanValidator(string dependentProperty) : base($"باید کمتر از {dependentProperty} باشد")
        {
            DependentProperty = dependentProperty;
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            if (context.PropertyValue == null)
            {
                return false;
            }

            var typeInfo = context.Instance.GetType();
            var dependentPropertyValue =
                Convert.ToInt32(typeInfo.GetProperty(DependentProperty).GetValue(context.Instance, null));

            return int.Parse(context.PropertyValue.ToString()) < dependentPropertyValue;
        }
    }

    public static class CustomFluentValidationExtensions
    {
        public static IRuleBuilderOptions<T, int> LowerThan<T>(
            this IRuleBuilder<T, int> ruleBuilder, string dependentProperty)
        {
            return ruleBuilder.SetValidator(new LowerThanValidator(dependentProperty));
        }
    }
}
- در اینجا dependentProperty همان خاصیت دیگری است که باید مقایسه با آن صورت گیرد. در متد بازنویسی شده‌ی IsValid، می‌توان مقدار آن‌را از context.Instance استخراج کرده و سپس با مقدار جاری context.PropertyValue مقایسه کرد.
- همچنین جهت سهولت کار فراخوانی آن، یک متد الحاقی جدید به نام LowerThan نیز در اینجا تعریف شده‌است.
- البته اگر قصد اعتبارسنجی سمت سرور را ندارید، می‌توان در متد IsValid، مقدار true را بازگشت داد.


2) ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور

در ادامه نیاز است ویژگی‌های data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Internal;
using FluentValidation.Resources;
using FluentValidation.Validators;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace FluentValidationSample.Models
{
    public class LowerThanClientValidator : ClientValidatorBase
    {
        private LowerThanValidator LowerThanValidator
        {
            get { return (LowerThanValidator)Validator; }
        }

        public LowerThanClientValidator(PropertyRule rule, IPropertyValidator validator) :
            base(rule, validator)
        {
        }

        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-LowerThan", GetErrorMessage(context));
            MergeAttribute(context.Attributes, "data-val-LowerThan-dependentproperty", LowerThanValidator.DependentProperty);
        }

        private string GetErrorMessage(ClientModelValidationContext context)
        {
            var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName());
            string messageTemplate;
            try
            {
                messageTemplate = Validator.Options.ErrorMessageSource.GetString(null);
            }
            catch (FluentValidationMessageFormatException)
            {
                messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>();
            }
            return formatter.BuildMessage(messageTemplate);
        }
    }
}
در این کدها، تنها قسمت مهم آن، متد AddValidation است که کار تعریف و افزودن متادیتاهای unobtrusive java script validation را انجام می‌دهد و برای مثال سبب رندر یک چنین تگ HTML ای می‌شود که در آن پیام اعتبارسنجی و خاصیت دیگر مدنظر ذکر شده‌اند:
<input dir="rtl" type="number"
 data-val="true"
 data-val-lowerthan="باید کمتر از Age باشد"
 data-val-lowerthan-dependentproperty="Age"
 data-val-required="'سابقه کار' must not be empty."
 id="Experience"
 name="Experience"
 value="">


3) تعریف مدل کاربران و اعتبارسنجی آن

مدلی که در این مثال از آن استفاده شده، یک چنین تعریفی را دارد و در قسمت اعتبارسنجی خاصیت Experience آن، از متد الحاقی جدید LowerThan استفاده شده‌است:
    public class UserModel
    {
        [Display(Name = "نام کاربری")]
        public string Username { get; set; }

        [Display(Name = "سن")]
        public int Age { get; set; }

        [Display(Name = "سابقه کار")]
        public int Experience { get; set; }
    }

    public class UserValidator : AbstractValidator<UserModel>
    {
        public UserValidator()
        {
            RuleFor(x => x.Username).NotNull();
            RuleFor(x => x.Age).NotNull();
            RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull();
        }
    }


4) افزودن اعتبارسنج‌های تعریف شده به تنظیمات برنامه

پس از تعریف LowerThanValidator و LowerThanClientValidator، روش افزودن آن‌ها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews().AddFluentValidation(
                fv =>
                {
                    fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
                    fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
                    fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;

                    fv.ConfigureClientsideValidation(clientSideValidation =>
                    {
                        clientSideValidation.Add(
                            validatorType: typeof(LowerThanValidator),
                            factory: (context, rule, validator) => new LowerThanClientValidator(rule, validator));
                    });
                }
            );
        }


5) تعریف کدهای جاوا اسکریپتی مورد نیاز

پیش از هرکاری، اسکریپت‌های فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
در اینجا مداخل jquery، سپس jquery.validate و بعد از آن jquery.validate.unobtrusive را مشاهده می‌کنید. در ادامه فایل js/site.js را به صورت زیر تکمیل خواهیم کرد:
$.validator.unobtrusive.adapters.add('LowerThan', ['dependentproperty'], function (options) {
    options.rules['LowerThan'] = {
        dependentproperty: options.params['dependentproperty']
    };
    options.messages['LowerThan'] = options.message;
});

$.validator.addMethod('LowerThan', function (value, element, parameters) {      
    var dependentProperty = '#' + parameters['dependentproperty'];
    var dependentControl = $(dependentProperty);
    if (dependentControl) {
        var targetvalue = dependentControl.val();
        if (parseInt(targetvalue) > parseInt(value)) {
            return true;
        }
        return false;
    }
    return true;
});
در اینجا یکبار اعتبارسنج LowerThan را به validator.unobtrusive معرفی می‌کنیم. سپس منطق پیاده سازی آن‌را که بر اساس یافتن مقدار خاصیت دیگر و مقایسه‌ی آن با مقدار خاصیت جاری است، به اعتبارسنج‌های jQuery Validator اضافه خواهیم کرد.


6) آزمایش برنامه

پس از این تنظیمات، اگر Viewما چنین تعریفی را داشته باشد:
@using FluentValidationSample.Models
@model UserModel
@{
    ViewData["Title"] = "Home Page";
}
<div dir="rtl">
    <form asp-controller="Home"
          asp-action="RegisterUser"
          method="post">
        <fieldset class="form-group">
        <legend>ثبت نام</legend>
            <div class="form-group row">
                <label asp-for="Username" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Username" class="form-control" />
                    <span asp-validation-for="Username" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group row">
                <label asp-for="Age" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Age" class="form-control" />
                    <span asp-validation-for="Age" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group row">
                <label asp-for="Experience" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Experience" class="form-control" />
                    <span asp-validation-for="Experience" class="text-danger"></span>
                </div>
            </div>

            <div class="form-group row">
                <label class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10 text-md-right">
                    <button type="submit" class="btn btn-info col-md-2">ارسال</button>
                </div>
            </div>
        </fieldset>
    </form>
</div>
در صورتیکه سابقه‌ی کار را بیشتر از سن وارد کنیم، به یک چنین خروجی سمت کلاینتی (بدون نیاز به post-back کامل به سمت سرور) خواهیم رسید:

مطالب
پیاده سازی عملیات CRUD در Kendo UI Treeview یک پروژه‌ی ASP.NET MVC
در این مقاله می‌خواهیم عملیات CRUD را بر روی Telerik kendo treeview  در یک پروژه‌ی ASP.NET MVC پیاده سازی کنیم. شکل کلی این پروژه به صورت زیر می‌باشد:


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

<div id="CrudPanel" class="row treeview-panel" >
      <div class="col-lg-7 pull-right">
           <input type="text" id="txtLocationTitle" class="form-control" />
      </div>
      <div class="col-lg-5 pull-left" style="text-align: left;">
           <button data-toggle="tooltip" data-placement="left" title="افزودن" id="btnAddLocation" class="btn btn-sm btn-success">
                <i class="fa fa-plus"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="عدم انتخاب" id="btnUnSelect" class="btn btn-sm btn-info">
                <i class="fa fa-square-o"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="ویرایش" id="btnEditLocation" class="btn btn-sm btn-warning">
                <i class="fa fa-pencil"></i>
           </button>
           <button data-toggle="tooltip" data-placement="left" title="حذف" id="btnDeleteLocation" class="btn btn-sm btn-danger">
                <i class="fa fa-times"></i>
           </button>
      </div>
</div>


و قطعه کد ذیل مربوط به پنل ویرایش است که در ابتدای کار کلاس hide به آن انتساب داده شده و پنهان می‌شود:

<div id="EditPanel" class="row edit hide treeview-panel">
     <div class="col-lg-7 pull-right">
          <input type="text" id="txtLocationEditTitle" class="form-control" />
     </div>
     <div class="col-lg-5 pull-left" style="text-align: left">
          <input type="button" value="ویرایش" id="btnEditPanelLocation" data-code="" data-parentId="" class="btn btn-sm btn-success" />
          <input type="button" value="انصراف" id="btnCancle" class="btn btn-sm btn-info" />
     </div>
</div>


در آخر این تکه کد نیز مربوط به KendoUI TreeView است:

 <div class="col-lg-6 k-rtl treeview-style">
                    @(Html.Kendo()
                          .TreeView()
                          .Name("treeview")
                          .DataTextField("Title")
                          .DragAndDrop(false)
                          .DataSource(dataSource => dataSource
                          .Model(model => model.Id("Id"))
                          .Read(read => read.Action(MVC.Admin.Location.ActionNames.GetAllAssetGroupTree, MVC.Admin.Location.Name)))
                    )
                </div>


یک نکته

- کلاس k-rtl مربوط به خود treeview می‌باشد و با این کلاس، درخت ما راست به چپ می‌شود.


در ادامه css‌های مربوط به کلاس‌های treeview-style ،hide و treeview-panel بررسی خواهند شد:

.treeview-style {
    min-height: 86px;
    max-height: 300px;
    overflow: scroll;
    overflow-x: hidden;
    position: relative;
}
.treeview-panel {
    background-color: #eee;
    padding: 25px 0 25px 0;
}
.hide {
    display: none;
}


تا اینجای مقاله، کدهای Html و Css موجود را بررسی کردیم. حالا سراغ قسمت اصلی خواهیم رفت. یعنی عملیات CRUD.


لازم به ذکر است در ابتدای قسمت script  باید این چند خط کد نوشته شود:

 var treeview = null;
    $(window).load(function () {
        treeview = $("#treeview").data("kendoTreeView");
    });

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


پیاده سازی عملیات افزودن: 

 $(document).on('click', '#btnAddLocation', function () {
        var title = $('#txtLocationTitle').val();
        var selectedNodeId = null;
        var selectedNode = treeview.select();
        if (selectedNode.length == 0) {
            selectedNode = null;
        }
        else {
            selectedNodeId = treeview.dataItem(selectedNode).id;// گرفتن آی دی گره انتخاب شده
        }
        $.ajax({
            url: '@Url.Action(MVC.Admin.Location.CreateByAjax())',
            type: 'POST',
            data: { Title: title, ParentId: selectedNodeId },
            success: function (data) {
                debugger;
                showMessage(data.message, data.notificationType);
                if (data.result)
                    treeview.dataSource.read();
            },
            error: function () {
                showMessage('لطفا مجددا تلاش نمایید', 'warning');
            }
        });

    });

توضیحات: مقدار گره جدید را خوانده و در متغیر title قرار می‌دهیم. گره انتخاب شده را توسط این خط

var selectedNode = treeview.select();

می گیریم و سپس در ادامه بررسی خواهیم کرد تا اگر گره‌ای انتخاب نشده باشد، به کاربر پیغامی را نشان دهد؛ در غیر این صورت توسط ajax، مقادیر مورد نظر، به اکشن ما در LocationController ارسال می‌شوند:

 [HttpPost]
        public virtual ActionResult CreateByAjax(AddLocationViewModel locationViewModel)
        {
            if (ModelState.IsNotValid())
                return JsonResult(false, "عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error);
            var result = _locationService.Add(locationViewModel);//سرویس مورد نظر برای اضافه کردن به دیتابیس
            switch (result)
            {
                case AddStatus.AddSuccessful:
                    _uow.SaveChanges();
                    return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success);
                case AddStatus.Faild:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
                case AddStatus.Exists:
                    return JsonResult(false, Messages.DataExists, NotificationType.Warning);
                default:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
            }
        }


   public virtual JsonResult JsonResult(bool result, string message, string notificationType)
        {
            return Json(new { result = result, message = message, notificationType = notificationType }, JsonRequestBehavior.AllowGet);
        }

اکشن JsonResult  که مقادیر نتیجه، پیغام و نوع اطلاع رسانی را می‌گیرد و یک آبجکت از نوع json را به تابع success ای‌جکس، ارسال می‌کند.


 public class AddLocationViewModel
    {
        [DisplayName("عنوان")]
        [Required(ErrorMessage ="لطفا عنوان گروه را وارد نمایید"),MinLength(2,ErrorMessage ="طول عنوان خیلی کوتاه می‌باشد ")]
        public string Title { get; set; }
        [DisplayName("گروه پدر")]
        public Guid? ParentId { get; set; }

    }

این کلاس viewModel ما می‌باشد.


  public enum AddStatus
    {
        AddSuccessful,
        Faild,
        Exists
    }

و این مورد هم کلاس AddStatus از نوع enum.


  public class Messages
    {
        #region  Fields

        public const string SaveSuccessfull = "اطلاعات با موفقیت ذخیره شد";
        public const string SaveFailed = "خطا در ثبت اطلاعات";
        public const string DeleteMessage = "کابر گرامی ، آیا از حذف کردن این رکورد مطمئن هستید ؟";
        public const string DeleteSuccessfull = "اطلاعات با موفقیت حذف شد";
        public const string DeleteFailed = "خطا در حذف اطلاعات ، لطفا مجددا تلاش نمایید";
        public const string DeleteHasInclude = "کاربر گرامی ، رکورد مورد نظر هم اکنون در بانک اطلاعاتی سیستم در حال استفاده توسط منابع دیگر می‌باشد";
        public const string NotFoundData = "اطلاعات یافت نشد";
        public const string NoAttachmentSelect = "تصویری انتخاب نشده است";
        public const string DataExists = "اطلاعات وارد شده در بانک اطلاعاتی موجود می‌باشد";
        public const string DeletedRowHasIncluded = "کاربر گرامی ، رکوردی که قصد حذف آن را دارید هم اکنون در بانک اطلاعاتی سیستم ، توسط سایر بخش‌ها در حال استفاده می‌باشد";
        
        #endregion
    }

و این موارد هم مقادیر ثابت فیلد‌های مورد استفاده‌ی ما در کلاس Message.


پیاده سازی عملیات حذف

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

$(document).on('click', '#btnDeleteLocation', function () {
        var selectedNode = treeview.select();
        var currentNode = treeview.dataItem(selectedNode);
        if (selectedNode.length == 0) {
            showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning');
        } else {
            var selectedNodeId = treeview.dataItem(selectedNode).id;
            if (currentNode.hasChildren) {
                var title = 'کاربر گرامی ، با حذف شدن این گره، تمام زیر شاخه‌های آن حذف می‌شود. آیا مطمئن هستید ؟ ';
                DeleteConfirm(selectedNodeId, '@Url.Action(MVC.Admin.Location.DeleteByAjax())', title);
            } else {
                $.ajax({
                    url: '@Url.Action(MVC.Admin.Location.DeleteByAjax())',
                    type: 'POST',
                    data: { id: selectedNodeId },
                    success: function (data) {
                        debugger;
                        showMessage(data.message, data.notificationType);
                        if (data.result)
                            treeview.remove(selectedNode);
                    },
                    error: function () {
                        showMessage('لطفا مجددا تلاش نمایید', 'warning');
                    }
                });
            }
        }
    });

این مورد نیز همانند عملیات افزودن عمل می‌کند. یعنی ابتدا چک می‌کند که آیا گره‌ای انتخاب شده است یا خیر؟ و اگر گره انتخابی ما دارای فرزند باشد، به کاربر پیغامی را نشان می‌دهد و می‌گوید «گره مورد نظر، دارای فرزند است. آیا مایل به حذف تمام فرزندان آن هستید؟» مانند تصویر زیر:



در نهایت چه گره انتخابی دارای فرزند باشد و چه نباشد، به یک مسیر مشترک ارسال می‌شوند:

  public virtual ActionResult DeleteByAjax(Guid id)
        {
            var result = _locationService.Delete(id);
            switch (result)
            {
                case DeleteStatus.Successfull:
                    _uow.SaveChanges();
                    return DeleteJsonResult(true, Messages.DeleteSuccessfull, NotificationType.Success);
                case DeleteStatus.NotFound:
                    return DeleteJsonResult(false, Messages.NotFoundData, NotificationType.Error);
                case DeleteStatus.Failed:
                    return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error);
                case DeleteStatus.ThisRowHasIncluded:
                    return DeleteJsonResult(false, Messages.DeletedRowHasIncluded, NotificationType.Warning);
                default:
                    return DeleteJsonResult(false, Messages.DeleteFailed, NotificationType.Error);
            }
        }


در سرویس مورد نظر ما یعنی Delete، اگه گره‌ای دارای فرزند باشد، تمام فرزندان آن را حذف می‌کند. حتی فرزندان فرزندان آن را:

  public DeleteStatus Delete(Guid id)
        {
            var model = GetAsModel(id);
            if (model == null) return DeleteStatus.NotFound;
            if (!CanDelete(model)) return DeleteStatus.ThisRowHasIncluded;
            _uow.MarkAsSoftDelete(model, _userManager.GetCurrentUserId());

            if (model.Children.Any())
                DeleteChildren(model);
            return DeleteStatus.Successfull;
        }


  private void DeleteChildren(Location model)
        {
            foreach (var item in model.Children)
            {
                _uow.MarkAsSoftDelete(item, _userManager.GetCurrentUserId());
                if (item.Children.Any())
                    DeleteChildren(item);
            }
        }


  public class Location:BaseEntity,ISoftDelete
    {
        public string Title { get; set; }
        public Location Parent { get; set; }
        public Guid? ParentId { get; set; }
        public bool IsDeleted { get; set; }

        public virtual ICollection<Location> Children { get; set; }
}

 و این هم مدل Location که سمت سرور از مدل استفاده می‌کنیم.


پیاده سازی عملیات ویرایش

حالا به قسمت اصلی مقاله رسیدیم. در اینجا قرار است گره‌ای را انتخاب نماییم و با زدن دکمه ویرایش و باز شدن پنل آن، آن را ویرایش کنیم. با زدن دکمه ویرایش، کدهای زیر اجرا می‌شوند:

    // Open Edit Panel
    $(document).on('click', '#btnEditLocation', function () {
        debugger;
        var selectedNode = treeview.select();
        var currentNode = treeview.dataItem(selectedNode);// با استفاده از این خط، گره انتخاب شده جاری را می‌گیریم.


        if (selectedNode.length == 0) {
//این شرط به ما می‌گوید اگر گره ای انتخاب نشده بود پیغامی به کاربر نمایش بده
            showMessage('گزینه ای انتخاب نشده است. لطفا یک گزینه انتخاب نمایید', 'warning');
        } else {
            var selectedNodeCode = treeview.dataItem(selectedNode).Code;
            var selectedNodeTitle = treeview.dataItem(selectedNode).Title;
            var selectedNodeParentId = treeview.dataItem(selectedNode).ParentId;
// آی دی یا کد، عنوان و آی دی پدر گره انتخاب شده را با استفاده از این سه خط در اختیار می‌گیریم
            $('#CrudPanel').toggleClass('hide'); //المنت کرادپنل که در حال حاضر کاربر آن را می‌بیند، با این خط کد، پنهان می‌شود
            $('#EditPanel').toggleClass('hide'); //المنت ادیت پنل که در حال حاضر از دید کاربر پنهان است، قابل نمایش می‌شود

            $("#txtLocationEditTitle").val(selectedNodeTitle);
//عنوان گره ای که می‌خواهیم آن را ویرایش کنیم در تکست باکس مورد نظر قرار می‌گیرد
            $("#txtLocationEditTitle").focusTextToEnd();
// با استفاده از این پلاگین، کرسر ماوس در انتهای مقدار دیفالت تکست باکس قرار می‌گیرد
            $("#btnEditPanelLocation").attr('data-code', selectedNodeCode);
            $("#btnEditPanelLocation").attr('data-parentId', selectedNodeParentId == null ? '' : selectedNodeParentId);
//مقادیر پرنت آی دی و کد را در دیتا اتریبیوت‌های موجود در المنت خودمان قرار می‌دهیم
            // Disable clicking in treeview
            $("#treeview").children().bind('click', function () { return false; });
        }
    });

  (function ($) {
        $.fn.focusTextToEnd = function () {
            this.focus();
            var $thisVal = this.val();
            this.val('').val($thisVal);
            return this;
        }
    }(jQuery));

کد زیر باعث می‌شود تا زمانیکه پنل ویرایش باز است، کاربر نتواند هیچ کلیکی را در عناصر داخل درخت ما، داشته باشد.

            $("#treeview").children().bind('click', function () { return false; });


و در نهایت با زدن دکمه ویرایش، پنل ویرایش ما به صورت زیر باز می‌شود:


همانطور که در تصویر بالا مشاهده می‌کنید، با انتخاب ساختمان مرکزی و زدن دکمه ویرایش، پنل CRUD ما پنهان و پنل ویرایش ظاهر می‌گردد. همچنین عنوان گره انتخابی به عنوان پیش فرض تکست باکس ما تنظیم می‌شود و کاربر نمی‌تواند گره دیگری را انتخاب کند؛ به شرط آنکه این پنل ویرایش بسته شود.

با تغییر عنوان تکست باکس و زدن دکمه‌ی ویرایش، رویداد زیر رخ می‌دهد:

  // Edit tree node
    $(document).on('click', '#btnEditPanelLocation', function () {
        debugger;
        var code = $("#btnEditPanelLocation").attr('data-code');
        var parentId = $("#btnEditPanelLocation").attr('data-parentId');
        var title = $("#txtLocationEditTitle").val().trim();
        $.ajax({
            url: '@Url.Action(MVC.Admin.Location.EditByAjax())',
            type: 'POST',
            data: { Code: code, Title: title, ParentId: parentId.length === 0 ? null : parentId },
            success: function (data) {
                debugger;
                showMessage(data.message, data.notificationType);
                if (data.result) {
                    treeview.dataSource.read();
                    CloseEditPanel();
                }
            },
            error: function () {
                showMessage('لطفا مجددا تلاش نمایید', 'warning');
            }
        });
    });


  [HttpPost]
        public virtual ActionResult EditByAjax(EditLocationViewModel editLocationViewModel)
        {

            if (ModelState.IsNotValid())
                return JsonResult(false,"عنوان نباید خالی و یا کمتر از دو کاراکتر باشد.", NotificationType.Error);
            var result = _locationService.Edit(editLocationViewModel);
            switch (result)
            {
                case EditStatus.Successful:
                    _uow.SaveChanges();
                    return JsonResult(true, Messages.SaveSuccessfull, NotificationType.Success);
                case EditStatus.NotFound:
                    return JsonResult(false, Messages.NotFoundData, NotificationType.Error);
                case EditStatus.Faild:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
                case EditStatus.Exists:
                    return JsonResult(false, Messages.DataExists, NotificationType.Warning);
                default:
                    return JsonResult(false, Messages.SaveFailed, NotificationType.Error);
            }
        }


تابع CloseEditPanel  بعد از اتمام ویرایش هر گره و یا با زدن دکمه انصراف در شکل بالا، فراخوانی می‌شود که کد آن به شکل زیر است:

  function CloseEditPanel() {
        $('#CrudPanel').toggleClass('hide');
//پنل کراد ما که در حال حاضر از دید کاربر پنهان است با این خط ظاهر می‌گردد
        $('#EditPanel').toggleClass('hide');
//پنل ویرایش ما که در حال حاضر کاربر آن را می‌بیند، پنهان می‌شود از دید کاربر
        $("#txtLocationEditTitle").val('');
//مقدار تکست باکس خالی می‌شود
        $("#btnEditPanelLocation").attr('data-code', '');
        $("#btnEditPanelLocation").attr('data-parentId', '');
//دیتا اتریبیوت‌های ما که مقادیر کد و آی دی والد در آن قرار گرفته نیز خالی می‌شود
        // Enable clicking in treeview
        $("#treeview").children().unbind('click').bind('click', function () { return true; });
//اگر یادتان باشد با یک خط کد به کاربر اجازه ندادیم که با باز شدن پنل ویرایش، گره دیگری را انتخاب نمایی. حالا این خط کد عکس کد قبلیست و به کاربر اجازه می‌دهد در المنت مورد نظر کلیک کند
    }


   // Cancle edit Node tree
    $(document).on('click', '#btnCancle', function () {
        CloseEditPanel();
    });
  $(document).on('click', '#btnUnSelect', function () {
//رویداد عدم انتخاب
        treeview.select(null);
    });
مطالب
چند نکته کاربردی درباره Entity Framework
1) رفتار متصل و غیر متصل در EF  چیست؟
اولین نکته ای که به ذهنم می‌رسه اینه که برای استفاده از EF حتما باید درک صحیحی از رفتارها و قابلیت‌های اون داشته باشیم.  نحوه استفاده ازٍEF  رو به دو رفتار متصل و غیر متصل  تقسیم می‌کنیم.
حالت پیش فرضEF  بر مبنای رفتار متصل می‌باشد. در این حالت شما یک موجودیت رو از دیتابیس فرا می‌خونید EF  این موجودیت رو ردگیری می‌کنه اگه تغییری در اون مشاهده کنه بر روی اون برچسب "تغییر داده شد" می‌زنه و حتی اونقدر هوشمند هست که وقتی تغییرات رو ذخیره می‌کنید کوئری آپدیت رو فقط براساس فیلدهای تغییر یافته اجرا کنه. یا مثلا در صورتی که شما بخواهید به یک خاصیت رابطه ای دسترسی پیدا کنید اگر قبلا لود نشده باشه در همون لحظه از دیتابیس فراخوانی میشه،  البته این رفتارها هزینه بر خواهد بود و در تعداد زیاد موجودیت‌ها میتونه کارایی رو به شدت پایین بیاره.
رفتار متصل شاید در ویندوز اپلیکیشن کاربرد داشته باشه ولی در حالت وب اپلیکیشن کاربردی نداره چون با هر در خواستی به سرور همه چیز از نو ساخته میشه و پس از پاسخ به درخواست همه چی از بین میره.  پس DbContext  همیشه از بین می‌ره و ما برحسب نیاز، در درخواست‌های جدید به سرور ، دوباره  DbContext   رو می‌سازیم. پس از ساخته شدن DbContext  باید موجودیت مورد استفاده رو به اون معرفی کنیم و وضعیت اون موجودیت رو هم مشخص کنیم.( جدید ، تغییر یافته، حذف ، بدون تغییر ) در این حالت سیستم ردگیری تغییرات بی استفاده است و ما فقط در حال هدر دادن منابع سیستم هستیم.
در حالت متصل ما باید همیشه از یک DbContext  استفاده کنیم و همه موجودیت‌ها در آخر باید تحت نظر این DbContext باشند در یک برنامه واقعی کار خیلی سخت و پیچیده ای است. مثلا بعضی وقت‌ها مجبور هستیم از  موجودیت هایی که قبلا در حافظه برنامه بوده اند استفاده کنیم اگر این موجودیت در حافظه DbContext  جاری وجود نداشته باشه با معرفی کردن اون از طریق متد attach کار ادامه پیدا می‌کنه ولی اگر قبلا موجودیتی  در  سیستم ردگیری DbContext با همین شناسه وجود داشته باشد با خطای زیر مواجه می‌شویم.
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key
 این خطا مفهوم ساده و مشخصی داره ، دو شی با یک شناسه نمی‌توانند در یک DbContext وجود داشته باشند. معمولا در این حالت ما بااین اشیا تکراری کاری نداریم و فقط به شناسه اون شی برای نشان دادن روابط نیاز داریم و از دیگر خاصیت‌های اون جهت نمایش به کاربر استفاده می‌کنیم ولی متاسفانه DbContext   نمی‌دونه چی تو سر ما می‌گذره و فقط حرف خودشو می‌زنه! البته اگه خواستید با DbContext  بر سر این موضوع گفتگو کنید از کدهای زیر استفاده کنید:
T attachedEntity = set.Find(entity.Id);
var attachedEntry = dbContext.Entry(attachedEntity);
attachedEntry.CurrentValues.SetValues(entity);
خوب با توجه به صحبت‌های بالا اگر بخواهیم از رفتار غیر متصل استفاده کنیم باید تنظیمات زیر رو به متد  سازنده DbContext   اضافه کنیم. از اینجا به بعد همه چیز رو خودمون در اختیار می‌گیریم و ما مشخص می‌کنیم که کدوم موجودیت باید چه وضعیتی داشته باشه (افزودن ، بروز رسانی، حذف )و اینکه چه موقع روابط خودش را با دیگر موجودیتها فراخوانی کنه.
public DbContext()
        {
            this.Configuration.ProxyCreationEnabled = false;
            this.Configuration.LazyLoadingEnabled = false;
            this.Configuration.AutoDetectChangesEnabled = false;
         }
2) تعیین وضعیت یک موجودیت و راوبط آن در  EF چگونه است؟
با کد زیر می‌تونیم وضعیت یک موجدیت رو مشخص کنیم ، با اجرای هر یک از دستورات زیر موجودیت تحت نظر DbContext قرار می‌گیره یعنی عمل attach نیز صورت گرفته است :
dbContext.Entry(entity).State = EntityState.Unchanged ;
dbContext.Entry(entity).State = EntityState.Added ; //or  Dbset.Add(entity)
dbContext.Entry(entity).State = EntityState.Modified ;
dbContext.Entry(entity).State = EntityState.Deleted ; // or  Dbset.Remove(entity)

با اجرای این کد موجودیت  از سیستم ردگیری DbContext خارج می‌شه.
 dbContext.Entry(entity).State = EntityState.Detached;
در موجودیت‌های ساده با دستورات بالا نحوه ذخیره سازی را مشخص می‌کنیم در وضعیتی که با موجودیت‌های رابطه ای سروکار داریم باید به نکات زیر توجه کنیم.
در نظر بگیرید یک گروه از قبل وجود دارد و ما مشتری جدیدی می‌سازیم در این حالت انتظار داریم که فقط یک مشتری جدید ذخیره شده باشد:
// group id=19  Name="General"  
var customer = new Customer();
customer.Group = group;
customer.Name = "mohammadi";
dbContext.Entry(customer).State = EntityState.Added;
var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added
var groupstate = dbContext.Entry(group);// groupstate=EntityState.Added

 اگه از روش بالا استفاده کنید می‌بینید گروه General   جدیدی به همراه مشتری در  دیتابیس ساخته می‌شود.نکته مهمی که اینجا وجود داره اینه که DbContext  به id  موجودیت گروه توجهی نداره ، برای جلو گیری از این مشکل باید قبل از معرفی موجودیت‌های جدید رابطه هایی که از قبل وجود دارند را به صورت بدون تغییر attach  کنیم و بعد وضعیت جدید موجودیت رو اعمال کنیم.
// group id=19  Name="General"  
var customer = new Customer();
customer.Group = group;
 customer.Name = "mohammadi";
dbContext.Entry(group).State = EntityState.Unchanged;
dbContext.Entry(customer).State = EntityState.Added;
var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added
 var groupstate = dbContext.Entry(group);// groupstate=EntityState.Unchanged

در مجموع بهتره که موجودیت ریشه رو attach کنیم و بعد با توجه به نیاز تغییرات رو اعمال کنیم.
  // group id=19  Name="General"  
var customer = new Customer();
 customer.Group = group;
customer.Name = "mohammadi";
dbContext.Entry(customer).State = EntityState.Unchanged;
dbContext.Entry(customer).State = EntityState.Added;
var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added
var groupstate = dbContext.Entry(group);//// groupstate=EntityState.Unchanged

3) AsNoTracking   و Include  دو ابزار مهم در رفتار غیر متصل:
درصورتیکه ما تغییراتی روی داده‌ها نداشته باشیم و یا از روش‌های غیر متصل از موجودیت‌ها استفاده کنیم با استفاده از متد AsNoTracking() در زمان و حافظه سیستم صرف جویی می‌کنیم در این حالت موجودیت‌های فراخوانی شده از دیتابیس در سیستم ردگیری DbContext قرار نمی‌گیرند و  اگر  وضعیت آنها را بررسی کنیم در وضعیت Detached قرار دارند.
var customer  = dbContext.Customers.FirstOrDefault();
 var customerAsNoTracking  = dbContext.Customers.AsNoTracking().FirstOrDefault();
var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Unchanged
var customerstateAsNoTracking  = dbContext.Entry(customerAsNoTracking).State;// customerstate=EntityState.Detached

نحوه بررسی کردن موجودیت‌های موجود در سیستم ردگیری DbContext :
var Entries = dbContext.ChangeTracker.Entries();    
var AddedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Added);
var ModifiedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Modified);
var UnchangedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Unchanged);
var DeletedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Deleted);
var DetachedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Detached);//* not working !
* در نظر داشته باشید وضعیت Detached وجود خارجی ندارد و به حالتی گفته می‌شود که DbContext در سیستم رد گیری خود اطلاعی از موجودیت مورد نظر نداشته باشد.
وقتی که سیستم فراخوانی خودکار رابطه‌ها خاموش باشد باید موقع فراخوانی موجودیت‌ها روابط مورد نیاز را هم با دستور Include  در سیستم فراخوانی کنیم.
 var CustomersWithGroup = dbContext.Customers.AsNoTracking().Include("Group").ToList();
 var CustomerFull = dbContext.Customers.AsNoTracking().Include("Group").Include("Bills").Include("Bills.BillDetails").ToList();
4) از متد AddOrUpdate در در فضای نام  System.Data.Entity.Migrations استفاده نکنیم، چرا؟
 در صورتی که از فیلد RowVersion و کنترل مسایل همزمانی استفاده کرده باشیم هر وقتی متد AddOrUpdate رو فراخوانی کنیم، تغییر اطلاعات توسط دیگر کاربران نادیده گرفته می‌شود.  با توجه به این که  متد AddOrUpdate  برای عملیات Migrations در نظر گرفته شده است، این رفتار کاملا طبیعی است. برای حل این مشکل می‌تونیم این متد رو با بررسی شناسه به سادگی پیاده سازی کنیم:
 public virtual void AddOrUpdate(T entity)
        {
            if (entity.Id == 0)
                Add(entity);
            else
                Update(entity);

        }

5) اگر بخواهیم موجودیت‌های رابطه ای در دیتا گرید ویو (ویندوز فرم) نشون بدیم باید چه کار کنیم؟

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

 public class DataGridViewChildRelationTextBoxCell : DataGridViewTextBoxCell
    {
        protected override object GetValue(int rowIndex)
        {
            try
            {
                var bs = (BindingSource)DataGridView.DataSource;
                var cl = (DataGridViewChildRelationTextBoxColumn)DataGridView.Columns[ColumnIndex];
                return getChildValue(bs.List[rowIndex], cl.DataPropertyName).ToString();
            }
            catch (Exception)
            {
                return "";
            }
        }

        private object getChildValue(object dataSource, string childMember)
        {
            int nextPoint = childMember.IndexOf('.');
            if (nextPoint == -1) return dataSource.GetType().GetProperty(childMember).GetValue(dataSource, null);
            string proName = childMember.Substring(0, nextPoint);
            object newDs = dataSource.GetType().GetProperty(proName).GetValue(dataSource, null);
            return getChildValue(newDs, childMember.Substring(nextPoint + 1));
        }
    }

    public class DataGridViewChildRelationTextBoxColumn : DataGridViewTextBoxColumn
    {
        public string DataMember { get; set; }

        public DataGridViewChildRelationTextBoxColumn()
        {
            CellTemplate = new DataGridViewChildRelationTextBoxCell();
        }
    }

نحوه استفاده را در ادامه می‌بینید. این روش توسط ویزارد گریدویو هم قابل استفاده است. موقع Add کردن Column نوع اون رو روی DataGridViewChildRelationTextBoxColumn   تنظیم کنید.

GroupNameColumn= new DataGridViewChildRelationTextBoxColumn(); //from your class
GroupNameColumn.HeaderText = "گروه مشتری";
GroupNameColumn.DataPropertyName = "Group.Name"; //EF  Property: Customer.Group.Name
GroupNameColumn.Visible = true;
GroupNameColumn.Width = 300;
DataGridView.Columns.Add(GroupNameColumn);