مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت پنجم
در این قسمت به بررسی بخش Collections (امکان ساخت گروه‌های شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسی‌های مختلف) ، بخش آگهی‌ها، سیستم لاگ عملیات کاربران و مدل‌های سیستمی می‌پردازیم.
در مدل‌های سیستم، یک تغییر کلی به منظور نگهداری آخرین تغییر دهنده و آخرین تاریخ تغییر در رکورد‌ها، ایجاد شده است. کلاس پایه‌ی زیر به منظور کپسوله کردن یکسری خصوصیات تکراری در نظر گرفته شده است.
  public abstract class BaseEntity
    {
        #region Properties
        /// <summary>
        /// gets or sets Identifier of this Entity
        /// </summary>
        public virtual long Id { get; set; }
        /// <summary>
        /// gets or sets date that this entity was created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets Date that this entity was updated
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets date that this entity repoted last time
        /// </summary>
        public virtual DateTime? ReportedOn { get; set; }
        /// <summary>
        /// gets or sets counter for Content's report
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// gets or sets TimeStamp for prevent concurrency Problems
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets ro sets User that Modify this entity
        /// </summary>
        public virtual User ModifiedBy { get; set; }
        /// <summary>
        /// gets ro sets Id of  User that modify this entity
        /// </summary>
        public virtual long? ModifiedById { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual User CreatedBy { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual long CreatedById { get; set; }
        #endregion
    }
با توجه به امکان تغییر نام کاربری توسط کاربر در سیستم، نگه داری صرفا نام کاربری آخرین تغییر دهنده، مفید نخواهد بود. شبیه به این کار را در سیستم Decision نیز می‌توانید مشاهده کنید. خصوصیاتی که نیاز به توضیح دارند :
  • ReportedOn : نگهداری آخرین تاریخ اخطار 
  • ModifyLocked : به منظور ممانعت از ویرایش 
  • CreatedBy,CreatedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان ایجاد کننده)
  • ModifiedBy , ModifiedById : به منظور ایجاد ارتباط یک به چند بین کاربر و سایر موجودیت‌ها (به عنوان آخرین تغییر دهنده)

مدل کلکسیون (کالکشن ،Collection)

بخش Collections می‌تواند برای کاربران امکان انتشار مطالب گروهبندی شده را بر اساس موضوع‌های مختلف، مهیا کند. کاربران بعد از کسب امتیازات لازم با استفاده از مهیا کردن محتوای وبلاگ یا فعالیت‌های آنها در انجمن و ... می‌توانند دسترسی لازم را برای ساخت Collections‌‌های خود، داشته باشند.  
 /// <summary>
    /// Represents the Collection for group posts by topic 
    /// </summary>
    public class Collection : GuidBaseEntity
    {
        #region Properties
        /// <summary>
        /// gets or sets name of collection
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// gets or sets Alternative SlugUrl
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets some description of group
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// gets or sets description that indicate how to pay 
        /// </summary>
        public virtual string HowToPay { get; set; }
        /// <summary>
        /// gets or sets Visibility Type 
        /// </summary>
        public virtual CollectionVisibility Visibility { get; set; }
        /// <summary>
        /// gets or sets color of Collection's Cover
        /// </summary>
        public virtual string Color { get; set; }
        /// <summary>
        /// gets or sets Name of Image that used as Cover
        /// </summary>
        public virtual string Photo { get; set; }
        /// <summary>
        /// gets or sets name of tags seperated by comma that assosiated with this content fo increase performance
        /// </summary>
        public virtual string TagNames { get; set; }
        /// <summary>
        /// indicate this collection is active or not
        /// </summary>
        public virtual bool IsActive { get; set; }
        #endregion

        #region  NavigationProperties
       
        /// <summary>
        /// get or set collection of attachments that attached in this group
        /// </summary>
        public virtual ICollection<CollectionAttachment> Attachments { get; set; }
        /// <summary>
        /// get or set tags of collection
        /// </summary>
        public virtual ICollection<Tag> Tags { get; set; }
        /// <summary>
        /// get or set List Of Posts that Associated with this Collection
        /// </summary>
        public virtual ICollection<CollectionPost> Posts { get; set; }
        /// <summary>
        /// get or set Users that they are Member of this collection if visibility is Custom
        /// </summary>
        public virtual ICollection<User> Memebers { get; set; }
        #endregion
    }
   public enum  CollectionVisibility
    {
        Friends,
        OnlyMe,
        Public,
        NotFree,
        Custom
    }
مدل بالا نشان دهنده‌ی کلکسیون‌های ایجاد شده‌ی توسط کاربران می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  • Visibility : برای اعمال محدودیت دسترسی به یک کلکسیون در نظر گرفته شده است. از نوع، نوع داده‌ی شمارشی CollectionVisibility پیاده سازی شده‌ی دربالا، می‌باشد. حالت Custom برای زمانی است که نیاز است صرفا یک سری از کاربران بدون هزینه‌، دسترسی برای مطالعه‌ی این کلکسیون داشته باشند. حالت NotFree هم برای زمانی است که کاربرانی که هزینه‌ی مورد نظر را پرداخت کرده باشند، به عنوان عضو به این کلکسیون می‌توانند دسترسی داشته باشند.
  • Members : به منظور اعمال ارتباط چند به چند بین مدل کاربر و مدل کلکسیون، در نظر گرفته شده است و زمانی این لیست اعضا خالی نیست که حالت Visibility با NotFree یا Custom مقدار دهی شده باشد.
  • Tags : برای یافتن راحت‌تر کلکسیون‌های مورد نظر کاربران، یک ارتباط چند به چند بین کلکسیون‌ها و مخزن برچسب مطرح شده‌ی در مقاله اول، در نظر گرفته شده است. 
  • TagNames : برای افزایش کارآیی سیستم در نظر گرفته شده است و نام برچسب‌های در ارتباط با کلکسیون را در خود به صورت جدا شده با (,) نگهداری می‌کند.
  • Posts : لیست پست‌هایی است که توسط مدیر کلکسیون در آن ارسال می‌شود. لذا یک ارتباط یک به چند بین کلکسیون‌ها و CollectionPost در نظر گرفته شده است.
  • Attachments : لیست فایل‌هایی است که در کلکسیون بارگذاری شده‌اند (در ادامه پیاده سازی خواهد شد).
  • Owner , OwnerId : هر کلکسیون نیز یک صاحب خواهد داشت. بدین منظور یک ارتباط یک به چند بین کاربر و کلکسیون برقرار شده است.
  • HowToPay : توضیحاتی در مورد نحوه‌ی پرداخت هزینه (در صورتی که Visibility این کلکسیون NotFree باشد).

مدل پست‌های کلکسیون ها

 public class CollectionPost : GuidBaseEntity
    {
        #region Properties
      
        /// <summary>
        /// indicate this post should be pin
        /// </summary>
        public virtual bool IsPin { get; set; }
        /// <summary>
        /// gets or sets the blog pot body
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets the content title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets value  indicating Custom Slug
        /// </summary>
        public virtual string SlugAltUrl { get; set; }
        /// <summary>
        /// gets or sets a value indicating whether the content comments are allowed 
        /// </summary>
        public virtual bool AllowComments { get; set; }
        /// <summary>
        /// indicate comments should be approved before display
        /// </summary>
        public virtual bool ModerateComments { get; set; }
        /// <summary>
        /// gets or sets viewed count 
        /// </summary>
        public virtual long ViewCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  approved comments
        /// <remarks>The same as if we run Item.Comments.Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int ApprovedCommentsCount { get; set; }
        /// <summary>
        /// Gets or sets the total number of  unapproved comments
        /// <remarks>The same as if we run Item.Comments.Count()
        /// We use this property for performance optimization (no SQL command executed)
        /// </remarks>
        /// </summary>
        public virtual int UnApprovedCommentsCount { get; set; }
        /// <summary>
        /// gets or sets rating complex instance
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets information of User-Agent
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// indicate users can share this post
        /// </summary>
        public virtual bool IsEnableForShare { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// get or set comments of this post
        /// </summary>
        public virtual ICollection<CollectionComment> Comments { get; set; }
       
        /// <summary>
        /// gets or sets collection that associated with this post
        /// </summary>
        public virtual Collection Collection { get; set; }
        /// <summary>
        /// gets or sets id of collection that associated with this post
        /// </summary>
        public virtual Guid CollectionId { get; set; }
        #endregion
    }
مدل بالا نشان دهنده‌ی پست‌های ارسالی در کلکسیون‌ها می‌باشد. خصوصیاتی که نیاز به توضیح دارند:
  • IsEnableForShare : اگر با مقدار True مقدار دهی شده باشد ، امکان به اشتراک گذاری آن درشبکه‌های اجتماعی وجود خواهد داشت. 
  • Comments : اگر مقدار AllowComments مربوط به پست ارسالی true باشد، در آن صورت امکان نظر دهی به پست  هم امکان پذیر خواهد بود. برای برقراری ارتباط یک به چند بین مدل پست کلکسیون و نظرات کلکسیون، این لیست در مدل بالا گنجانده شده است.
  • ModerateComments : اگر برای پست خاصی، با مقدار true مقدار دهی شده باشد، نظرات آن پست قبل از نمایش باید توسط مدیر آن کلکسیون تأیید شوند.
  • Author , AuthorId : در واقع ارسال کننده‌ی تمام پست‌ها، همان صاحب کلکسیون می‌باشد. ولی برای راحتی واکشی لیست پست‌های ارسالی کاربر مورد نظر یک ارتباط یک به چند بین کاربر و پست‌های ارسالی را در کلکسیون اعمال کرده‌ایم.
  • Collection , CollectionId : کلید خارجی ما در دیتابیس ایجاد شده خواهد بود که نشان دهنده‌ی ارتباط یک به چند بین کلکسیون و پست‌ها می‌باشد.
  • IsPin : اگر لازم است پستی به عنوان اولین پست در کلکسیون نمایش داده شود، این خصوصیت برای آن true خواهد بود.
  • ApprovedCommentsCount , UnApprovedCommentsCount: برای افزایش کارآیی سیستم در نظر گرفته شده است و هنگام درج نظر جدید یا حذف نظر، ویرایش خواهند شد. 

مدل نظرات ارسالی در کلکسیون ها

 public class CollectionComment 
    {
        #region Ctor
        public CollectionComment()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            CreatedOn = DateTime.Now;

        }
        #endregion

        #region Properties
        /// <summary>
        /// get or set identifier of record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets date of creation 
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets body of blog post's comment
        /// </summary>
        public virtual Rating Rating { get; set; }
        /// <summary>
        /// gets or sets informations of agent
        /// </summary>
        public virtual string UserAgent { get; set; }
        /// <summary>
        /// indicate this comment is Approved 
        /// </summary>
        public virtual bool IsApproved { get; set; }
        /// <summary>
        /// gets or sets Ip Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime? ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets counter for report this comment
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// gets or sets date that this entity repoted last time
        /// </summary>
        public virtual DateTime? ReportedOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets post that this comment added it
        /// </summary>
        public virtual CollectionPost Post { get; set; }
        /// <summary>
        /// gets or sets id of post that this comment added it
        /// </summary>
        public virtual Guid PostId { get; set; }
        /// <summary>
        /// get or set user that create this record
        /// </summary>
        public virtual User Creator { get; set; }
        /// <summary>
        /// get or set Id of user that create this record
        /// </summary>
        public virtual long CreatorId { get; set; }
        /// <summary>
        /// gets or sets CollectionComment's identifier for Replying and impelemention self referencing
        /// </summary>
        public virtual Guid? ReplyId { get; set; }
        /// <summary>
        /// gets or sets Collection's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual CollectionComment Reply { get; set; }
        /// <summary>
        /// get or set collection of Collection's comment for Replying and impelemention self referencing
        /// </summary>
        public virtual ICollection<CollectionComment> Children { get; set; }
        #endregion
    }

مدل بالا نشان دهنده‌ی نظرات ارسالی برای پست‌های کلکسیون‌ها می‌باشد. صرفا کاربران عضو سیستم این اجازه را در صورتی خواهند داشت که برای پست مورد نظر خصوصیت AllowComments با مقدار true مقدار دهی شده باشد
حالت درختی آن مشخص است. برای اعمال ارتباط یک به چند بین پست‌ها و نظرات، از CollectionPost و CollectionPostId استفاده خواهد شد.

  • IsApproved : برای زمانی استفاده خواهد شد که خصوصیت ModerateComments پست مورد نظر با مقدار true مقدار دهی شده باشد. 
  • ReportsCount : به مانند بخش‌های قبل، تعداد اخطار‌های داده شده‌ی برای یک نظر را نشان خواهد داد. 
  • Creator,CreatorId : ارسال کننده‌ی نظر می‌باشد و برای ایجاد ارتباط یک به چند بین کاربر و نظرات کلکسیون‌ها در نظر گرفته شده‌اند. 
  • ReportedOn : نگه داری آخرین تاریخ اخطار 
  • ModifyLocked : به منظور ممانعت از ویرایش

مدل فایل‌های ضمیمه کلکسیون ها

public class CollectionAttachment : BaseAttachment
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets Collection  that this file attached 
        /// </summary>
        public virtual Collection Collection { get; set; }
        /// <summary>
        /// gets or sets Id of Collection  that this file attached
        /// </summary>
        public virtual Guid? CollectionId { get; set; }
        #endregion
    }
اگر یادتان باشد در مقاله‌ی دوم در مورد نحوه‌ی مدیریت فایل‌ها بحث شد و در نتیجه تصمیم گرفته شد که از ارث بری TPH استفاده کنیم. مدل بالا نیز یکی از SubClass‌های ما خواهد بود. با توجه به اینکه شاید Privacy فایل‌های ارسالی یک گروه مهم باشد (در صورت خصوصی بودن یا پولی بودن مطالعه‌ی کلکسیون)  نیاز شد مدل بالا را نیز داشته باشیم. برای اعمال ارتباط یک به چند بین مدل بالا و مدل کلکسیون، از خصوصیت‌های Colllection , CollectionId استفاده خواهیم کرد. دقت کنید که لازم است CollectionId به صورت نال پذیر درنظر گرفته شود.

مدل آگهی ها

/// <summary>
    /// Represents Announcement For Announcement Section
    /// </summary>
    public class Announcement : BaseContent
    {
        #region Properties
        /// <summary>
        /// gets or sets Date that this Announcement will Expire
        /// </summary>
        public virtual DateTime? ExpireOn { get; set; }
        /// <summary>
        /// indicate this accouncement is approved by admin if announcementSetting.Moderate==true
        /// </summary>
        public virtual bool IsApproved { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// get or set Collection of Comments for this Announcement
        /// </summary>
        public virtual ICollection<AnnouncementComment> Comments { get; set; }

        #endregion
    }
مدل بالا نشان دهنده‌ی آگهی‌ها سیستم خواهد بود. همان طور که مشخص است، این مدل نیز از کلاس پایه BaseContent ارث بری کرده و علاوه بر آن یکسری خصوصیت دیگر را به شرح زیر دارد:
  • ExpireOn : زمان انقضای آگهی 
  • IsApproved : به منظور اعمال مدیریتی در نظر گرفته شده است
  • Comments : اگر امکان ارسال نظرات برای آگهی از بخش تنظیمات فعال باشد، این لیست نظرات ما را نگه داری خواهد کرد. لذا یک رابطه‌ی یک به چند بین نظرات و آگهی‌ها خواهد بود.

مدل نظرات آگهی ها

/// <summary>
    /// Repersents Comment For Announcement
    /// </summary>
    public class AnnouncementComment : BaseComment
    {
        #region NavigationProperties
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual AnnouncementComment Reply { get; set; }
        /// <summary>
        /// gets or sets body of announcement's comment
        /// </summary>
        public virtual ICollection<AnnouncementComment> Children { get; set; }
        /// <summary>
        /// gets or sets announcement that this comment sent to it
        /// </summary>
        public virtual Announcement Announcement { get; set; }
        /// <summary>
        /// gets or sets announcement'Id that this comment sent to it
        /// </summary>
        public virtual long AnnouncementId { get; set; }
        #endregion
    }
مدل بالا  نشان دهنده‌ی نظرات ارسالی برای آگهی‌ها می‌باشد. از کلاس پایه‌ی مطرح شده‌ی در مقاله‌ی اول ارث بری کرده و علاوه بر آن ساختار درختی آن مشخص است و همچنین برای برقراری ارتباط یک به چند با مدل آگهی‌ها، خصوصیات Announcement  , AnnouncementId در نظر گرفته شده‌اند.

مدل سیستم لاگ عملیات کاربران

/// <summary>
    /// Represent The Operation's log
    /// </summary>
    public class AuditLog
    {
        #region Ctor
        /// <summary>
        /// 
        /// </summary>
        public AuditLog()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            OperatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier of AuditLog
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// sets or gets description of Log
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// sets or gets when log is operated
        /// </summary>
        public virtual DateTime OperatedOn { get; set; }
        /// <summary>
        /// sets or gets log's section
        /// </summary>
        public virtual AuditSection Section { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets log's creator
        /// </summary>
        public virtual User OperatedBy { get; set; }
        /// <summary>
        /// sets or gets identifier of log's creator
        /// </summary>
        public virtual long OperatedById { get; set; }
        #endregion
    }

  public enum AuditSection
    {
        Blog,
        News,
        Forum,
        ...
    }
مدل بالا نگهدارنده زمان اکشن انجام شده، توسط کاربری که انجام شده و یه سری توضیحات کلی می‌باشد. به منظور اعمال ارتباط یک به چند مابین کاربر و مدل بالا، خصوصیات OperatedBy و OperatedById را در نظر گرفته‌ایم. خصوصیت Section که از نوع، نوع داده شمارشی AuditSection می‌باشد، می‌تواند برای جداسازی این لاگ‌های ذخیره شده، مفید باشد.

مدل دامین‌های ممنوع

در پنل مدیریت امکانی را خواهیم داشت تا یکسری از دامین‌های مد نظر را که نمی‌خواهیم در محتوای سیستم، آدرس صفحات آنها دیده شوند و همچنین برای عدم ارسال و دریافت پینگ بک‌ها لحاظ شوند، ثبت کنیم.
 /// <summary>
    /// Represents Domain that is banned
    /// </summary>
    public class BannedDomain
    {
        #region Propertie
        /// <summary>
        /// gets or sets identifier of Domain
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets DomainName
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// gets or sets Date that this record added
        /// </summary>
        public virtual DateTime BannedOn { get; set; }
        #endregion
    }

مدل کلمات ممنوع 

 /// <summary>
    /// Represents the banned words
    /// </summary>
    public class BannedWord
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="BannedWord"/>
        /// </summary>
        public BannedWord()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier of BannedWord
        /// </summary>
        public Guid Id { get; set; }
        /// <summary>
        /// gets or sets Bad word
        /// </summary>
        public string BadWord { get; set; }
        /// <summary>
        /// gets or sets Good replaceword
        /// </summary>
        public string GoodWord { get; set; }
        /// <summary>
        /// indicating that this Word is spam
        /// </summary>
        public bool IsStopWord { get; set; }
        #endregion

    }
مدل بالا نشان دهنده‌ی کلماتی است که لازم است در متن مطالب سیستم حذف یا با کلمات جدید جایگزین شوند.
  • BadWord : کلمه مورد نظر که قرار است Ban شود.
  • IsStopWord : اگر لازم نیست جایگزینی برای کلمه استفاده شود و فقط لازم است حذف گردد، مقدار این خصوصیت true خواهد بود.
  • GoodWord : کلمه جایگزین 

مدل تنظیمات سیستم

 /// <summary>
    /// Represent The CMS setting
    /// </summary>
    public class Setting
    {
        #region Properties
        /// <summary>
        /// sets or gets name of setting
        /// </summary>
        public virtual string Name { get; set; }
        /// <summary>
        /// sets or gets value of setting
        /// </summary>
        public virtual string Value { get; set; }
        /// <summary>
        /// sets or gets Type of setting
        /// </summary>
        public virtual string Type { get; set; }
        #endregion
    }
مدل بالا کل تنظیمات سیستم را در قالب Key , Value نگهداری خواهد کرد. برای توضیحات این قسمت می‌توانید به مقاله‌ی "ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF" رجوع کنید.
پ.ن:در مقاله‌ی بعد با پیاده سازی مدل‌های مدیریت کاربران، سیستم پیام رسانی، سیستم ترفیع رتبه و ارتباط دوستی، DomainClasses به اتمام خواهد رسید.

نتیجه‌ی این قسمت 

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

مطالب
استفاده از LINQ to XML جهت خواندن فیدهای RSS

مثال زیر را به عنوانی نمونه‌ای از کاربرد LINQ to XML برای خواندن فیدهای RSS که اساسا به فرمت XML هستند می‌توان ارائه داد.
ابتدا کد کامل مثال را در نظر بگیرید:

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

namespace LinqToRSS
{
public static class LanguageExtender
{
public static string SafeValue(this XElement input)
{
return (input == null) ? string.Empty : input.Value;
}

public static DateTime SafeDateValue(this XElement input)
{
return (input == null) ? DateTime.MinValue : DateTime.Parse(input.Value);
}
}

public class RssEntry
{
public string Title { set; get; }
public string Description { set; get; }
public string Link { set; get; }
public DateTime PublicationDate { set; get; }
public string Author { set; get; }
public string BlogName { set; get; }
public string BlogAddress { set; get; }
}

public class Rss
{
static XElement selectDate(XElement date1, XElement date2)
{
return date1 ?? date2;
}

public static List<RssEntry> GetEntries(string feedUrl)
{
//applying namespace in an XElement
XName xn = XName.Get("{http://purl.org/dc/elements/1.1/}creator");//{namespace}root
XName xn2 = XName.Get("{http://purl.org/dc/elements/1.1/}date");

var feed = XDocument.Load(feedUrl);
if (feed.Root == null) return null;

var items = feed.Root.Element("channel").Elements("item");
var feedQuery =
from item in items
select new RssEntry
{
Title = item.Element("title").SafeValue(),
Description = item.Element("description").SafeValue(),
Link = item.Element("link").SafeValue(),
PublicationDate =
selectDate(item.Element(xn2), item.Element("pubDate")).SafeDateValue(),
Author = item.Element(xn).SafeValue(),
BlogName = item.Parent.Element("title").SafeValue(),
BlogAddress = item.Parent.Element("link").SafeValue()
};

return feedQuery.ToList();
}
}

class Program
{
static void Main(string[] args)
{
List<RssEntry> entries = Rss.GetEntries("http://weblogs.asp.net/aspnet-team/rss.aspx");
if (entries != null)
foreach (var item in entries)
Console.WriteLine(item.Title);

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

توضیحات:
1- در این مثال فقط جهت سهولت بیان آن در یک صفحه، تمامی کلاس‌های تعریف شده در یک فایل آورده شدند. این روش صحیح نیست و باید به ازای هر کلاس یک فایل جدا در نظر گرفته شود.
2- کلاس LanguageExtender از قابلیت extension methods سی شارپ 3 استفاده می‌کند. به این صورت کلاس XElement دات نت بسط یافته و دو متد به آن اضافه می‌شود که به سادگی در کدهای خود می‌توان از آن‌ها استفاده کرد. هدف آن هم بررسی نال بودن یک آیتم دریافتی و ارائه‌ی حاصلی امن برای این مورد است.
3- کلاس RssEntry به جهت استفاده در خروجی کوئری LINQ تعریف شد. می‌خواهیم خروجی نهایی، یک لیست جنریک از نوع RssEntry باشد.
4- متد اصلی برنامه، GetEntries است. این متد آدرس اینترنتی یک فید را دریافت کرده و پس از آنالیز، آن‌را به صورت یک لیست بر می‌گرداند.

<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://weblogs.asp.net/utility/FeedStylesheets/rss.xsl" media="screen"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>Latest Microsoft Blogs</title>
<link>http://weblogs.asp.net/aspnet-team/default.aspx</link>
<description />
<dc:language>en</dc:language>
<generator>CommunityServer 2007 SP1 (Build: 20510.895)</generator>
<item>
<title>Comments on my recent benchmarks.</title>
<link>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/</link>
<pubDate>Mon, 10 Aug 2009 23:33:59 GMT</pubDate>
<guid isPermaLink="false">c06e2b9d-981a-45b4-a55f-ab0d8bbfdc1c:7166225</guid>
<dc:creator>Misfit Geek: msft</dc:creator>
<slash:comments>0</slash:comments>
<wfw:commentRss xmlns:wfw="http://wellformedweb.org/CommentAPI/">http://weblogs.asp.net/aspnet-team/rsscomments.aspx?PostID=7166225</wfw:commentRss>
<comments>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/#comments</comments>
<description>Overall I’ve been pretty impressed ...</description>
<category domain="http://weblogs.asp.net/aspnet-team/archive/tags/ASP.NET/default.aspx">ASP.NET</category>
</item>
</channel>
</rss>
برای نمونه خروجی یک فید می‌تواند به صورت فوق باشد. آیتم‌های آن به صورت قابل بیان است:
var items = feed.Root.Element("channel").Elements("item");
و نکته مهمی که اینجا وجود دارد، اعمال فضاهای نام بکار رفته در این فایل xml پیشرفته می‌باشد. برای اعمال فضاهای نام به یکی از دو روش زیر می‌توان عمل کرد:

XName.Get("{mynamespace}root");
//or
XName.Get("root", "mynamespace");

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

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

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

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

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

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

<h1>Attribute Splatting</h1>

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

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

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

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

<h1>Attribute Splatting</h1>

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

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



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

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

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

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

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

<h1>Attribute Splatting</h1>

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


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


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

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

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


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

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

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


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


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

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

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

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

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

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


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


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-09.zip
مطالب
بارگذاری یک یوزرکنترل با استفاده از جی‌کوئری

مزیت استفاده از یوزر کنترل‌ها، ماژولار کردن برنامه است. برای مثال اگر صفحه جاری شما قرار است از چهار قسمت اخبار، منوی پویا ، سخن روز و آمار کاربران تشکیل شود، می‌توان هر کدام را توسط یک یوزر کنترل پیاده سازی کرده و سپس صفحه اصلی را از کنار هم قرار دادن این یوزر کنترل‌ها تهیه نمود.
با این توضیحات اکنون می‌خواهیم یک یوزکنترل ASP.Net را توسط jQuery Ajax بارگذاری کرده و نمایش دهیم. حداقل دو مورد کاربرد را می‌توان برای آن متصور شد:
الف) در اولین باری که یک صفحه در حال بارگذاری است، قسمت‌های مختلف آن‌را بتوان از یوزر کنترل‌های مختلف خواند و تا زمان بارگذاری کامل هر کدام، یک عبارت لطفا منتظر بمانید را نمایش داد. نمونه‌ی آن‌را شاید در بعضی از CMS های جدید دیده باشید. صفحه به سرعت بارگذاری می‌شود. در حالیکه مشغول مرور صفحه جاری هستید، قسمت‌های مختلف صفحه پدیدار می‌شوند.
ب) بارگذاری یک قسمت دلخواه صفحه بر اساس درخواست کاربر. مثلا کلیک بر روی یک دکمه و امثال آن.

روش کلی کار:
1) تهیه یک متد وب سرویس که یوزر کنترل را بر روی سرور اجرا کرده و حاصل را تبدیل به یک رشته کند.
2) استفاده از متد Ajax جی‌کوئری برای فراخوانی این متد وب سرویس و افزودن رشته دریافت شده به صفحه.
بدیهی است زمانیکه متد Ajax فراخوانی می‌شود می‌توان عبارت یا تصویر منتظر بمانید را نمایش داد و پس از پایان کار این متد، عبارت (یا تصویر) را مخفی نمود.

پیاده سازی:
قسمت تبدیل یک یوزر کنترل به رشته را قبلا در مقاله "تهیه قالب برای ایمیل‌های ارسالی یک برنامه ASP.Net" مشاهده کرده‌اید. در این‌جا برای استفاده از این متد در یک وب سرویس نیاز به کمی تغییر وجود داشت (KeyValuePair ها درست سریالایز نمی‌شوند) که نتیجه نهایی به صورت زیر است. یک فایل Ajax.asmx را به برنامه اضافه کرده و سپس در صفحه Ajax.asmx.cs کد آن به صورت زیر می‌تواند باشد:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Script.Services;
using System.Web.Services;
using System.Web.UI;
using System.Web.UI.HtmlControls;

namespace AjaxTest
{
public class KeyVal
{
public string Key { set; get; }
public object Value { set; get; }
}

/// <summary>
/// Summary description for Ajax
/// </summary>
[ScriptService]
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class Ajax : WebService
{
/// <summary>
/// Removes Form tags using Regular Expression
/// </summary>
private static string cleanHtml(string html)
{
return Regex.Replace(html, @"<[/]?(form)[^>]*?>", string.Empty, RegexOptions.IgnoreCase);
}

/// <summary>
/// تبدیل یک یوزر کنترل به معادل اچ تی ام ال آن
/// </summary>
/// <param name="path">مسیر یوزر کنترل</param>
/// <param name="properties">لیست خواص به همراه مقادیر مورد نظر</param>
/// <returns></returns>
/// <exception cref="NotImplementedException"><c>NotImplementedException</c>.</exception>
[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public string RenderUserControl(string path,
List<KeyVal> properties)
{
Page pageHolder = new Page();

UserControl viewControl =
(UserControl)pageHolder.LoadControl(path);

viewControl.EnableViewState = false;

Type viewControlType = viewControl.GetType();

if (properties != null)
foreach (var pair in properties)
{
if (pair.Key != null)
{
PropertyInfo property =
viewControlType.GetProperty(pair.Key);

if (property != null)
{
if (pair.Value != null) property.SetValue(viewControl, pair.Value, null);
}
else
{
throw new NotImplementedException(string.Format(
"UserControl: {0} does not have a public {1} property.",
path, pair.Key));
}
}
}

//Form control is mandatory on page control to process User Controls
HtmlForm form = new HtmlForm();

//Add user control to the form
form.Controls.Add(viewControl);

//Add form to the page
pageHolder.Controls.Add(form);

//Write the control Html to text writer
StringWriter textWriter = new StringWriter();

//execute page on server
HttpContext.Current.Server.Execute(pageHolder, textWriter, false);

// Clean up code and return html
return cleanHtml(textWriter.ToString());
}
}
}
تا این‌جا متد وب سرویسی را داریم که می‌تواند مسیر یک یوزر کنترل را به همراه خواص عمومی آن‌را دریافت کرده و سپس یوزر کنترل را رندر نموده و حاصل را به صورت HTML به شما تحویل دهد. با استفاده از reflection خواص عمومی یوزر کنترل یافت شده و مقادیر لازم به آن‌ها پاس می‌شوند.

چند نکته:
الف) وب کانفیگ برنامه ASP.Net شما اگر با VS 2008 ایجاد شده باشد مداخل لازم را برای استفاده از این وب سرویس توسط jQuery Ajax دارد در غیر اینصورت موفق به استفاده از آن نخواهید شد.
ب) هنگام بازگرداندن این اطلاعات با فرمت json = ResponseFormat.Json جهت استفاده در jQuery Ajax ، گاهی از اوقات بسته به حجم بازگردانده شده ممکن است خطایی حاصل شده و عملیات متوقف شد. این طول پیش فرض را (maxJsonLength) در وب کانفیگ به صورت زیر تنظیم کنید تا مشکل حل شود:

<system.web.extensions>
<scripting>
<webServices>
<jsonSerialization maxJsonLength="10000000"></jsonSerialization>
</webServices>
</scripting>
</system.web.extensions>

برای پیاده سازی قسمت Ajax آن برای اینکه کار کمی تمیزتر و با قابلیت استفاده مجدد شود یک پلاگین تهیه شده (فایلی با نام jquery.advloaduc.js) که سورس آن به صورت زیر است:

$.fn.advloaduc = function(options) {
var defaults = {
webServiceName: 'Ajax.asmx', //نام فایل وب سرویس ما
renderUCMethod: 'RenderUserControl', //متد وب سرویس
ucMethodJsonParams: '{path:\'\'}',//پارامترهایی که قرار است پاس شوند
completeHandler: null //پس از پایان کار وب سرویس این متد جاوا اسکریپتی فراخوانی می‌شود
};
var options = $.extend(defaults, options);

return this.each(function() {
var obj = $(this);
obj.prepend("<div align='center'> لطفا اندکی تامل بفرمائید... <img src=\"images/loading.gif\"/></div>");

$.ajax({
type: "POST",
url: options.webServiceName + "/" + options.renderUCMethod,
data: options.ucMethodJsonParams,
contentType: "application/json; charset=utf-8",
dataType: "json",
success:
function(msg) {
obj.html(msg.d);

// if specified make callback and pass element
if (options.completeHandler)
options.completeHandler(this);
},
error:
function(XMLHttpRequest, textStatus, errorThrown) {
obj.html("امکان اتصال به سرور در این لحظه مقدور نیست. لطفا مجددا سعی کنید.");
}
});
});
};
برای اینکه با کلیات این روش آشنا شوید می‌توان به مقاله "بررسی وجود نام کاربر با استفاده از jQuery Ajax در ASP.Net" مراجعه نمود که از ذکر مجدد آن‌ها خودداری می‌شود. همچنین در مورد نوشتن یک پلاگین جی‌کوئری در مقاله "افزونه جملات قصار jQuery" توضیحاتی داده شده است.
عمده کاری که در این پلاگین صورت می‌گیرد فراخوانی متد Ajax جی‌کوئری است. سپس به متد وب سرویس ما (که در اینجا نام آن به صورت پارامتر نیز قابل دریافت است)، پارامترهای لازم پاس شده و سپس نتیجه حاصل به یک شیء در صفحه اضافه می‌شود.
completeHandler آن اختیاری است و پس از پایان کار متد اجکس فراخوانی می‌شود. در صورتیکه به آن نیازی نداشتید یا مقدار آن را null قرار دهید یا اصلا آن‌را ذکر نکنید.

مثالی در مورد استفاده از این وب سرویس و همچنین پلاگین جی‌کوئری نوشته شده:

الف) یوزر کنترل ساده زیر را به پروژه اضافه کنید:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="part1.ascx.cs" Inherits="TestJQueryAjax.part1" %>
<asp:Label runat="server" ID="lblData" ></asp:Label>
بدیهی است یک یوزر کنترل می‌تواند به اندازه یک صفحه کامل پیچیده باشد به همراه انواع و اقسام ارتباطات با دیتابیس و غیره.

سپس کد آن‌را به صورت زیر تغییر دهید:

using System;
using System.Threading;

namespace TestJQueryAjax
{
public partial class part1 : System.Web.UI.UserControl
{
public string Text1 { set; get; }
public string Text2 { set; get; }

protected void Page_Load(object sender, EventArgs e)
{
Thread.Sleep(3000);
if (!string.IsNullOrEmpty(Text1) && !string.IsNullOrEmpty(Text2))
lblData.Text = Text1 + "<br/>" + Text2;
}
}
}
این یوزر کنترل دو خاصیت عمومی دارد که توسط وب سرویس مقدار دهی خواهد شد و نهایتا حاصل نهایی را در یک لیبل در دو سطر نمایش می‌دهد.
عمدا یک sleep سه ثانیه‌ای در اینجا در نظر گرفته شده تا اثر آن‌را بهتر بتوان مشاهده کرد.

ب) اکنون کد مربوط به صفحه‌ای که قرار است این یوزر کنترل را به صورت غیرهمزمان بارگذاری کند به صورت زیر خواهد بود (مهم‌ترین قسمت آن نحوه تشکیل پارامترها و مقدار دهی خواص یوزر کنترل است):

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="TestJQueryAjax._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>

<script src="js/jquery.js" type="text/javascript"></script>
<script src="js/jquery.advloaduc.js" type="text/javascript"></script>
<script src="js/json2.js" type="text/javascript"></script>

<script type="text/javascript">
function showAlert() {
alert('finished!');
}

//تشکیل پارامترهای متد وب سرویس جهت ارسال به آن
var fileName = 'part1.ascx';
var props = [{ 'Key': 'Text1', 'Value': 'سطر یک' }, { 'Key': 'Text2', 'Value': 'سطر 2'}];
var jsonText = JSON.stringify({ path: fileName, properties: props });

$(document).ready(function() {
$("#loadMyUc").advloaduc({
webServiceName: 'Ajax.asmx',
renderUCMethod: 'RenderUserControl',
ucMethodJsonParams: jsonText,
completeHandler: showAlert
});
});

</script>

</head>
<body>
<form id="form1" runat="server">
<div id="loadMyUc">
</div>
</form>
</body>

</html>
نکته:
برای ارسال صحیح و امن اطلاعات json به سرور، از اسکریپت استاندارد json2.js استفاده شد.

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


دریافت مثال فوق


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


مدل لغو اعمال

پایه لغو اعمال، توسط مکانیزمی به نام CancellationToken پیاده سازی شده‌است و آن‌را به عنوان یکی از آرگومان‌های متدهایی که لغو اعمال را پشتیبانی می‌کنند، مشاهده خواهید کرد. به این ترتیب یک عمل خاص می‌تواند دریابد چه زمانی لغو آن درخواست شده‌است. البته باید دقت داشت که این عملیات بر مبنای ایده‌ی ‌همه یا هیچ است. به این معنا که یک درخواست لغو را بار دیگر نمی‌توان لغو کرد.


یک مثال استفاده از CancellationToken

کدهای زیر، یک فایل حجیم را از مکانی به مکانی دیگر کپی می‌کنند. برای این منظور از متد CopyToAsync که در دات نت 4.5 اضافه شده‌است، استفاده کرده‌ایم؛ زیرا از مکانیزم لغو عملیات پشتیبانی می‌کند.
using System;
using System.IO;
using System.Threading;

namespace Async08
{
    class Program
    {
        static void Main(string[] args)
        {
            var source = @"c:\dir\file.bin";
            var target = @"d:\dir\file.bin";
            using (var inStream = File.OpenRead(source))
            {
                using (var outStream = File.OpenWrite(target))
                {
                    using (var cts = new CancellationTokenSource())
                    {
                        var task = inStream.CopyToAsync(outStream, bufferSize: 4059, cancellationToken: cts.Token);
                        Console.WriteLine("Press 'c' to cancel.");
                        var key = Console.ReadKey().KeyChar;
                        if (key == 'c')
                        {
                            Console.WriteLine("Cancelling");
                            cts.Cancel();
                        }
                        Console.WriteLine("Wating...");
                        task.ContinueWith(t => { }).Wait();
                        Console.WriteLine("Status: {0}", task.Status);
                    }
                }
            }
        }
    }
}
کار با تعریف CancellationTokenSource شروع می‌شود. چون از نوع IDisposable است، نیاز است توسط عبارت using، جهت پاکسازی منابع آن، محصور گردد. سپس در اینجا اگر کاربر کلید c را فشار دهد، متد لغو توکن تعریف شده فراخوانی خواهد شد. این توکن نیز به عنوان آرگومان به متد CopyToAsync ارسال شده‌است.
علت استفاده از ContinueWith در اینجا این است که اگر یک task لغو شود، فراخوانی متد Wait بر روی آن سبب بروز استثناء می‌گردد. به همین جهت توسط ContinueWith یک Task خالی ایجاد شده و سپس بر روی آن Wait فراخوانی گردیده‌است.
همچنین باید دقت داشت که سازنده‌ی CancellationTokenSource امکان دریافت زمان timeout عملیات را نیز دارد. به علاوه متد CancelAfter نیز برای آن طراحی شده‌است. نمونه‌ی دیگری از تنظیم timeout را در قسمت قبل با معرفی متد Task.Delay و استفاده از آن با Task.WhenAny مشاهده کردید.


لغو ظاهری وظایفی که لغو پذیر نیستند

فرض کنید متدی به نام GetBitmapAsync با پارامتر cancellationToken طراحی نشده‌است. در این حالت کاربر قصد دارد با کلیک بر روی دکمه‌ی لغو، عملیات را خاتمه دهد. یک روش حل این مساله، استفاده از متد ذیل است:
    public static class CancellationTokenExtensions
    {
        public static async Task UntilCompletionOrCancellation(Task asyncOp, CancellationToken ct)
        {
            var tcs = new TaskCompletionSource<bool>();
            using (ct.Register(() => tcs.TrySetResult(true)))
            {
                await Task.WhenAny(asyncOp, tcs.Task);
            }
        }
    }
در اینجا از روش Task.WhenAny استفاده شده‌است که در آن دو task ترکیب شده‌اند. Task اول همان وظیفه‌ای اصلی است و task دوم، از یک TaskCompletionSource حاصل شده‌است. اگر کاربر دستور لغو را صادر کند، callback ثبت شده توسط این توکن، اجرا خواهد شد. بنابراین در اینجا TrySetResult به true تنظیم شده و یکی از دو Task معرفی شده در WhenAny خاتمه می‌یابد.
این مورد هر چند task اول را واقعا لغو نمی‌کند، اما سبب خواهد شد تا کدهای پس از await UntilCompletionOrCancellation اجرا شوند.


طراحی متدهای غیرهمزمان لغو پذیر

کلاس زیر را در نظر بگیرید:
    public class CancellationTokenTest
    {
        public static void Run()
        {
            var cts = new CancellationTokenSource();
            Task.Run(async () => await test(), cts.Token);
            Console.ReadLine();
            cts.Cancel();
            Console.WriteLine("Cancel...");
            Console.ReadLine();
        }

        private static async Task test()
        {
            while (true)
            {
                await Task.Delay(1000);
                Console.WriteLine("Test...");
            }
        }
    }
در اینجا cancellationToken متد Task.Run تنظیم شده‌است. همچنین پس از فراخوانی آن، اگر کاربر کلیدی را فشار دهد، متد Cancel این توکن فراخوانی خواهد شد. اما .... خروجی برنامه به صورت زیر است:
Test...
Test...
Test...
 
Cancel...
Test...
Test...
Test...
Test...
بله. وظیفه‌ی شروع شده، لغو شده‌است اما متد test آن هنوز مشغول به کار است.
روش اول حل این مشکل، معرفی پارامتر CancellationToken به متد test و سپس بررسی مداوم خاصیت IsCancellationRequested آن می‌باشد:
public class CancellationTokenTest
    {
        public static void Run()
        {
            var cts = new CancellationTokenSource();
            Task.Run(async () => await test(cts.Token), cts.Token);
            Console.ReadLine();
            cts.Cancel();
            Console.WriteLine("Cancel...");
            Console.ReadLine();
        }

        private static async Task test(CancellationToken ct)
        {
            while (true)
            {
                await Task.Delay(1000, ct);
                Console.WriteLine("Test...");

                if (ct.IsCancellationRequested)
                {
                    break;
                }
            }
            Console.WriteLine("Test cancelled");
        }
    }
در اینجا اگر متد cts.Cancel فراخوانی شود، مقدار خاصیت ct.IsCancellationRequested مساوی true شده و حلقه خاتمه می‌یابد.
روش دوم لغو عملیات، استفاده از متد Register است. هر زمان که توکن لغو شود، callback آن فراخوانی خواهد شد:
        private static async Task test2(CancellationToken ct)
        {
            bool isRunning = true;

            ct.Register(() =>
            {
                isRunning = false;
                Console.WriteLine("Query cancelled");
            });

            while (isRunning)
            {
                await Task.Delay(1000, ct);
                Console.WriteLine("Test...");
            }
            Console.WriteLine("Test cancelled");
        }
این روش خصوصا برای حالت‌هایی مفید است که در آن‌ها از متدهایی استفاده می‌شود که خودشان امکان لغو شدن را نیز دارند. به این ترتیب دیگر نیازی نیست مدام بررسی کرد که آیا مقدار IsCancellationRequested مساوی true شده‌است یا خیر. هر زمان که callback ثبت شده در متد Register فراخوانی شد، یعنی عملیات باید خاتمه یابد.
مطالب
WPF4 و ویندوز 7 : به خاطر سپاری لیست آخرین فایل‌های گشوده شده توسط برنامه

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




خبر خوب اینکه برای اضافه کردن این قابلیت به برنامه‌های WPF4 نیازی به کد نویسی نیست و این موارد که تحت عنوان استفاده از Jump list ویندوز 7 تعریف شده‌اند، با کمی دستکاری فایل App.Xaml برنامه، فعال می‌گردند:

<Application x:Class="WpfApplication1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
<JumpList.JumpList>
<JumpList ShowRecentCategory="True" />
</JumpList.JumpList>
</Application>
همین! از این پس هر فایلی که توسط برنامه‌ی شما با استفاده از common file dialog boxes باز شود به صورت خودکار به لیست مذکور اضافه می‌گردد (بدیهی است Jump lists جزو ویژگی‌های ویندوز 7 است و در سایر سیستم عامل‌ها ندید گرفته خواهد شد).


سؤال: من اینکار را انجام دادم ولی کار نمی‌کنه!؟

پاسخ: بله. کار نمی‌کنه! این قابلیت تنها زمانی فعال خواهد شد که علاوه بر نکته‌ی فوق، پسوند فایل یا فایل‌هایی نیز به برنامه‌ی شما منتسب شده باشد. این انتساب‌ها مطلب جدیدی نیست و در تمام برنامه‌های ویندوزی باید توسط بکارگیری API ویندوز مدیریت شود. قطعه کد زیر اینکار را انجام خواهد داد:
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32;

namespace Common.Files
{
//from : http://www.devx.com/vb2themax/Tip/19554?type=kbArticle&trk=MSCP
public class FileAssociation
{
const int ShcneAssocchanged = 0x8000000;
const int ShcnfIdlist = 0;

public static void CreateFileAssociation(
string extension,
string className,
string description,
string exeProgram)
{
// ensure that there is a leading dot
if (extension.Substring(0, 1) != ".")
extension = string.Format(".{0}", extension);

try
{
if (IsAssociated(extension)) return;

// create a value for this key that contains the classname
using (var key1 = Registry.ClassesRoot.CreateSubKey(extension))
{
if (key1 != null)
{
key1.SetValue("", className);
// create a new key for the Class name
using (var key2 = Registry.ClassesRoot.CreateSubKey(className))
{
if (key2 != null)
{
key2.SetValue("", description);
// associate the program to open the files with this extension
using (var key3 = Registry.ClassesRoot.CreateSubKey(string.Format(@"{0}\Shell\Open\Command", className)))
{
if (key3 != null) key3.SetValue("", string.Format(@"{0} ""%1""", exeProgram));
}
}
}
}
}

// notify Windows that file associations have changed
SHChangeNotify(ShcneAssocchanged, ShcnfIdlist, 0, 0);
}
catch (Exception ex)
{
//todo: log ...
}
}

// Return true if extension already associated in registry
public static bool IsAssociated(string extension)
{
return (Registry.ClassesRoot.OpenSubKey(extension, false) != null);
}

[DllImport("shell32.dll")]
public static extern void SHChangeNotify(int wEventId, int uFlags, int dwItem1, int dwItem2);
}
}
و مثالی از نحوه‌ی استفاده از آن:
private static void createFileAssociation()
{
var appPath = Assembly.GetExecutingAssembly().Location;
FileAssociation.CreateFileAssociation(".xyz", "xyz", "xyz File",
appPath
);
}
لازم به ذکر است که این کد در ویندوز 7 فقط با دسترسی مدیریتی قابل اجرا است (کلیک راست و اجرا به عنوان ادمین) و در سایر حالات با خطای Access is denied متوقف خواهد شد. به همین جهت بهتر است برنامه‌ی نصاب مورد استفاده این نوع انتسابات را مدیریت کند؛ زیرا اکثر آن‌ها با دسترسی مدیریتی است که مجوز نصب را به کاربر جاری خواهند داد. اگر از فناوری Click once استفاده می‌کنید به این مقاله و اگر برای مثال از NSIS کمک می‌گیرید به این مطلب مراجعه نمائید.


سؤال: من این کارها را هم انجام دادم. الان به چه صورت از آن‌ استفاده کنم؟

زمانیکه کاربری بر روی یکی از این فایل‌های ذکر شده در لیست آخرین فایل‌های گشوده شده توسط برنامه کلیک کند، آدرس این فایل به صورت یک آرگومان به برنامه ارسال خواهد شد. برای مدیریت آن در WPF باید به فایل App.Xaml.cs مراجعه کرده و چند سطر زیر را به آن افزود:
    public partial class App
{
public App()
{
this.Startup += appStartup;
}

void appStartup(object sender, StartupEventArgs e)
{
if (e.Args.Any())
{
this.Properties["StartupFileName"] = e.Args[0];
}
}
//...

در این کد، e.Args حاوی مسیر فایل انتخابی است. برای مثال در اینجا مقدار آن به خاصیت StartupFileName انتساب داده شده است. این خاصیت در برنامه‌های WPF به صورت یک خاصیت عمومی تعریف شده است و در سراسر برنامه (مثلا در رخداد آغاز فرم اصلی آن یا هر جای دیگری) به صورت زیر قابل دسترسی است:
var startupFileName = Application.Current.Properties["StartupFileName"];

سؤال: برنامه‌ی من از OpenFileDialog برای گشودن فایل‌ها استفاده نمی‌کند. آیا راه دیگری برای افزودن مسیرهای باز شده به Jump lists ویندوز 7 وجود دارد؟

پاسخ: بله. همانطور که می‌دانید عناصر XAML با اشیاء دات نت تناظر یک به یک دارند. به این معنا که JumpList تعریف شده در ابتدای این مطلب در فایل App.XAML ، دقیقا معادل کلاسی به همین نام در دات نت فریم ورک است (تعریف شده در فضای نام System.Windows.Shell) و با کد نویسی نیز قابل دسترسی و مدیریت است. برای مثال:
var jumpList = JumpList.GetJumpList(App.Current);
var jumpPath = new JumpPath();
jumpPath.Path = "some path goes here....";
// If the CustomCategory property is null
// or Empty, the item is added to the Tasks category
jumpPath.CustomCategory = "Files";
JumpList.AddToRecentCategory(jumpPath);
jumpList.Apply();
به همین ترتیب،‌ JumpPath ذکر شده در کدهای فوق، در کدهای XAML نیز قابل تعریف است:
<Application x:Class="Win7Wpf4.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
<JumpList.JumpList>
<JumpList ShowRecentCategory="True">
<JumpPath
CustomCategory="Files"
Path="Some path goes here..."
/>
</JumpList>
</JumpList.JumpList>
</Application>

مطالب
React 16x - قسمت 2 - بررسی پیشنیازهای جاوا اسکریپتی - بخش 1
برای کار با React، نیاز است با ES6 آشنایی داشته باشید که در این سایت، یک سری کامل بررسی مقدمات آن‌را پیشتر مرور کرده‌ایم. علاوه بر توصیه‌ی مطالعه‌ی این سری (اینکار الزامی است)، در این قسمت خلاصه‌ی بسیار سریع و کاربردی آن‌را که بیشتر در برنامه‌های مبتنی بر React مورد استفاده قرار می‌گیرند، با هم مرور خواهیم کرد. در قسمت‌های بعدی، اهمیت ذکر این خلاصه بیشتر مشخص می‌شود.

برای بررسی ویژگی‌های جاوا اسکریپت مدرن، یک پروژه‌ی جدید React را ایجاد می‌کنیم.
> create-react-app sample-02
> cd sample-02
> npm start
سپس تمام کدهای داخل index.js را نیز حذف می‌کنیم. اکنون تمام کدهای خالص جاوا اسکریپتی خود را داخل این فایل خواهیم نوشت.
به علاوه چون در این قسمت خروجی UI نخواهیم داشت، تمام خروجی را در کنسول developer tools مرورگر خود می‌توانید مشاهده کنید (فشردن دکمه‌ی F12).


var، let و const

در اکثر زبان‌های برنامه نویسی، متغیرها در محدوده‌ی دید قطعه کدی که تعریف شده‌اند (scope)، قابل دسترسی هستند. برای نمونه محتوای فایل index.js پروژه را به صورت زیر تغییر داده و با فرض اجرای دستور npm start، خروجی آن‌را می‌توان در کنسول مرورگر مشاهده کرد.
function sayHello() {
  for (var i = 0; i < 5; i++) {
    console.log(i);
  }
  console.log(i);
}

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


در آخرین پیمایش حلقه، i مساوی 5 شده و از حلقه خارج می‌شود. اما چون در اینجا برای تعریف متغیر از واژه‌ی کلیدی var استفاده شده‌است، محدوده‌ی دید آن به کل تابعی که در آن تعریف شده‌است، بسط پیدا می‌کند. به همین جهت در این خروجی، عدد 5 را نیز مشاهده می‌کند که حاصل دسترسی به i، خارج از حلقه‌است.
برای یک دست سازی این رفتار با سایر زبان‌های برنامه نویسی، در ES6، واژه‌ی کلیدی جدیدی به نام let تعریف شده‌است که میدان دید متغیر را به قطعه کدی که در آن تعریف شده‌است، محدود می‌کند. اکنون اگر در حلقه‌ی فوق بجای var از let استفاده شود، یک چنین خطایی در مرورگر ظاهر خواهد شد که عنوان می‌کند، i استفاده شده‌ی در خارج از حلقه، تعریف نشده‌است.
./src/index.js
  Line 14:15:  'i' is not defined  no-undef

Search for the keywords to learn more about each error.

علاوه بر let، واژه‌ی کلیدی جدید const نیز به ES6 اضافه شده‌است که از آن برای تعریف ثوابت استفاده می‌شود. constها نیز همانند let، میدان دید محدود شده‌ای به قطعه کد تعریف شده‌ی در آن دارند؛ اما قابلیت انتساب مجدد را ندارند:
 const x = 1;
x = 2; // Attempting to override 'x' which is a constant.
اگر یک چنین قطعه کدی را اجرا کنیم، خطای x is const را در مرورگر می‌توان مشاهده کرد.

به صورت خلاصه از این پس واژه‌ی کلیدی var را فراموش کنید. همیشه با const جهت تعریف متغیرها شروع کنید. اگر به خطا برخوردید و نیاز به انتساب مجدد وجود داشت، آن‌را به let تغییر دهید. بنابراین استفاده از const همیشه نسبت به let ارجحیت دارد.


اشیاء در جاوا اسکریپت

اشیاء در جاوا اسکریپت به صورت مجموعه‌ای از key/value‌ها تعریف می‌شوند:
const person = {
  name: "User 1",
  walk: function() {}, // method
  talk() {} // concise method
};
در اینجا امکان تعریف یک تابع نیز وجود دارد که چون درون یک شیء قرار می‌گیرد، اینبار «متد» نامیده می‌شود. همچنین در ES6 می‌توان این متدها را به صورت معمولی، مانند متد talk نیز تعریف کرد که به آن‌ها concise method می‌گویند. بنابراین نحوه‌ی تعریف فوق را به نحو زیر نیز می‌توان خلاصه کرد:
const person = {
  name: "User 1",
  walk() {},
  talk() {}
};
پس از تعریف این شیء، روش دسترسی به اجزای آن به صورت زیر است:
person.talk();
person.name = "User 3";
person["name"] = "User 2";
به دو مورد اول، روش dot notation می‌گویند که از همان ابتدا دقیقا مشخص است کدامیک از خواص و متدهای شیء تعریف شده، مورد استفاده قرار می‌گیرند.
مورد آخر همان روش استفاده از key/valueها است که اساس اشیاء جاوا اسکریپتی را تشکیل می‌دهد. البته از این روش فقط زمانی استفاده کنید که قرار است یکسری کار پویا صورت گیرند (مقدار key به صورت متغیر دریافت شود) و از ابتدا مشخص نیست که کدام خاصیت یا متد قرار است تعریف و استفاده شود:
const targetMember = "name";
person[targetMember] = "User 2";


واژه‌ی کلیدی this در جاوا اسکریپت

از واژه‌ی کلیدی this، در قسمت‌های بعدی زیاد استفاده خواهیم کرد. به همین جهت نیاز است تفاوت‌های اساسی آن‌را با سایر زبان‌های برنامه نویسی بررسی کنیم.
همان شیء person را که پیشتر تعریف کردیم درنظر بگیرید. در متد walk آن، مقدار this را لاگ می‌کنیم:
const person = {
  name: "User 1",
  walk() {
    console.log(this);
  },
  talk() {}
};

person.walk();
خروجی این قطعه، به صورت زیر است:


شیء this در جاوا اسکریپت، همانند سایر زبان‌های برنامه نویسی مانند سی‌شارپ و یا جاوا رفتار نمی‌کند. در سایر زبان‌های نامبرده شده، this همواره ارجاعی را به وهله‌ای از شیء جاری، باز می‌گرداند؛ دقیقا همانند تصویری که در بالا مشاهده می‌کنید. در اینجا نیز this جاوا اسکریپتی لاگ شده، ارجاعی را به وهله‌ی جاری شیء person، بازگشت داده‌است. اما مشکل اینجا است که this در جاوا اسکریپت، همیشه به این صورت رفتار نمی‌کند!
برای نمونه در ادامه یک ثابت را به نام walk تعریف کرده و آن‌را به person.walk مقدار دهی می‌کنیم:
const walk = person.walk;
console.log(walk);
دقت داشته باشید که در اینجا از () استفاده نشده‌است (متد walk اجرا نشده‌است). یعنی صرفا «ارجاعی» از متد walk شیء person را به ثابت walk نسبت داده‌ایم. بنابراین اکنون ثابت walk نیز یک function است که حاصل console.log آن به صورت زیر است:


سؤال: اکنون اگر این function را با فراخوانی ()walk اجرا کنیم، چه خروجی را می‌توان مشاهده کرد؟


اینبار this لاگ شده، به شیء person اشاره نمی‌کند و شیء استاندارد window مرورگر را بازگشت داده‌است!
اگر یک function به صورت متدی از یک شیء فراخوانی شود، مقدار this همواره اشاره‌گری به وهله‌ای از آن شیء خواهد بود. اما اگر این تابع به صورت متکی به خود و به صورت یک function و نه متد یک شیء، فراخوانی شود، اینبار this، شیء سراسری جاوا اسکریپت یا همان شیء window را بازگشت می‌دهد.

یک نکته: اگر strict mode جاوا اسکریپت را در پروژه‌ی جاری فعال کنیم، بجای شیء window، مقدار undefined را در خروجی فوق شاهد خواهیم بود.


اتصال مجدد this به شیء اصلی در جاوا اسکریپت

تا اینجا دریافتیم که اگر یک function را به صورت متکی به خود و نه جزئی از یک شیء فراخوانی کنیم، شیء this در این حالت به شیء window سراسری مرورگر اشاره می‌کند و اگر strict mode فعال باشد، فقط undefined را بازگشت می‌هد. اکنون می‌خواهیم بررسی کنیم که چگونه می‌توان این مشکل را برطرف کرد؛ یعنی صرف‌نظر از نحوه‌ی فراخوانی متدها یا تابع‌ها، this همواره ارجاعی را به شیء person بازگشت دهد.
در جاوا اسکریپت، تابع‌ها نیز شیء هستند. برای مثال person.walk نوشته شده نیز یک شیء است. برای اثبات ساده‌ی آن فقط یک دات را پس از person.walk قرار دهید:


همانطور که مشاهده می‌کنید، شیء person.walk مانند تمام اشیاء دیگر جاوا اسکریپت، به همراه متد bind نیز هست. کار آن، انقیاد یک تابع، به یک شیء است. یعنی هرچیزی را که به عنوان آرگومان آن، به آن ارسال کنیم، به عنوان مقدار شیء this درنظر می‌گیرد:
const walk2 = person.walk.bind(person);
console.log(walk2);
walk2();
در اینجا متد bind، یک وهله‌ی جدید از person.walk را بازگشت می‌دهد که در آن شیء person را به عنوان شیء this، تنظیم کرده‌است. به همین جهت اینبار فراخوانی walk2 که به شیء person متصل شده‌است، به this صحیحی بجای window سراسری اشاره می‌کند. از این روش در برنامه‌های مبتنی بر React زیاد استفاده می‌شود.


Arrow functions

تابع زیر را درنظر بگیرید که به یک ثابت انتساب داده شده‌است:
const square = function(number) {
  return number * number;
};
در ES6، روش ساده‌تر و تمیزتری برای این نوع تعاریف، ذیل ویژگی جدید Arrow functions اضافه شده‌است. برای تبدیل قطعه کد فوق به یک arrow function، ابتدا واژه‌ی کلیدی function را حذف می‌کنیم. سپس بین پارامتر تابع و {}، یک علامت <= (که به آن fat arrow هم می‌گویند!) قرار می‌دهیم:
const square2 = (number) => {
  return number * number;
};
اگر مانند اینجا تنها یک تک پارامتر وجود داشته باشد، می‌توان پرانتزهای ذکر شده را نیز حذف کرد:
const square2 = number => {
  return number * number;
};
و اگر این متد پارامتری نداشت، از () استفاده می‌شود.
در ادامه اگر بدنه‌ی این تابع، فقط حاوی یک return بود، می‌توان آن‌را به صورت زیر نیز خلاصه کرد (در اینجا {} به همراه واژه‌ی کلیدی return حذف می‌شوند):
const square3 = number => number * number;
console.log(square3(5));
این یک سطر ساده شده، دقیقا معادل اولین const square ای است که نوشتیم. نحوه‌ی فراخوانی آن نیز مانند قبل است.

اکنون مثال مفید دیگری را در مورد Arrow functions بررسی می‌کنیم که بیشتر شبیه به عبارات LINQ در #C است:
const jobs = [
  { id: 1, isActive: true },
  { id: 2, isActive: true },
  { id: 3, isActive: true },
  { id: 4, isActive: true },
  { id: 5, isActive: false }
];
در اینجا آرایه‌ای از اشیاء job را مشاهده می‌کنید که مورد آخر آن، فعال نیست. اکنون می‌خواهیم لیست کارهای فعال را گزارشگیری کنیم:
var activeJobs = jobs.filter(function(job) {
  return job.isActive;
});
متد filter در جاوا اسکریپت، بر روی تک تک عناصر آرایه‌ی jobs حرکت می‌کند. سپس هر job را به پارامتر متد ارسالی آن که predicate نام دارد، جهت دریافت یک خروجی true و یا false، ارائه می‌دهد. اگر خروجی این متد true باشد، آن job انتخاب خواهد شد و در لیست نهایی گزارش، ظاهر می‌شود.
در ادامه می‌توان این تابع را توسط arrow functions به صورت ساده‌تر زیر نیز نوشت:
var activeJobs2 = jobs.filter(job => job.isActive);
ابتدا واژه‌ی کلیدی function را حذف می‌کنیم. سپس چون یک تک پارامتر را دریافت می‌کند، نیازی به ذکر پرانتزهای آن نیز نیست. در ادامه چون یک تک return را داریم، { return }  را با یک <= جایگزین خواهیم کرد. در اینجا نیازی به ذکر سمی‌کالن انتهای return هم نیست. نوشتن یک چنین کدی تمیزتر و خواندن آن، ساده‌تر است.


ارتباط بین arrow functions و شیء this

نکته‌ی مهمی را که باید در مورد arrow functions دانست این است که شیء this را rebind نمی‌کنند (rebind: مقدار دهی مجدد؛ ریست کردن).
در مثال زیر، ابتدا شیء user با متد talk که در آن شیء this، لاگ شده، ایجاد شده و سپس این متد فراخوانی گردیده‌است:
const user = {
  name: "User 1",
  talk() {
    console.log(this);
  }
};
user.talk();
همانطور که انتظار می‌رود، this ای که در اینجا لاگ می‌شود، دقیقا ارجاعی را به وهله‌ی جاری شیء user دارد.
اکنون اگر متد لاگ کردن را داخل یک تایمر قرار دهیم چه اتفاقی رخ می‌دهد؟
const user = {
  name: "User 1",
  talk() {
    setTimeout(function() {
      console.log(this);
    }, 1000);
  }
};
user.talk();
متد setTimeout، متدی را که به عنوان آرگومان اول آن دریافت کرده، پس از 1 ثانیه اجرا می‌کند.
در این حالت خروجی console.log، مجددا همان شیء سراسری window مرورگر است و دیگر به وهله‌ی جاری شیء user اشاره نمی‌کند. علت اینجا است که پارامتر اول متد setTimeout که یک callback function نام دارد، جزئی از هیچ شیءای نیست. بنابراین دیگر مانند فراخوانی متد ()user.talk در مثال قبلی کار نمی‌کند؛ چون متکی به خود است. هر زمان که یک متد متکی به خود غیر وابسته‌ی به یک شیء را اجرا کنیم، به صورت پیش‌فرض this آن، به شیء window مرورگر اشاره می‌کند.

سؤال: چگونه می‌توان درون یک callback function متکی به خود، به this همان شیء user جاری دسترسی یافت؟
یک روش حل این مساله، ذخیره this شیء user در یک متغیر و سپس ارسال آن به متد متکی به خود setTimeout است:
const user2 = {
  name: "User 2",
  talk() {
    var self = this;
    setTimeout(function() {
      console.log(self);
    }, 1000);
  }
};
user2.talk();
این روشی است که سال‌ها است وجود دارد؛ با ارائه‌ی arrow functions، دیگر نیازی به اینکار نیست و می‌توان از روش زیر استفاده کرد:
const user3 = {
  name: "User 3",
  talk() {
    setTimeout(() => console.log(this), 1000);
  }
};
user3.talk();
در اینجا callback function را تبدیل به یک arrow function کرده‌ایم و چون arrow functions شیء this را rebind نمی‌کنند، یعنی شیء this را به ارث می‌برند. بنابراین console.log مثال فوق، دقیقا به this شیء user اشاره می‌کند و دوباره آن‌را مقدار دهی مجدد نمی‌کند و از همان نمونه‌ی موجود قطعه کد تعریف شده، استفاده خواهد کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-02.zip

در قسمت بعد نیز بررسی پیشنیازهای جاوا اسکریپتی شروع به کار با React را ادامه خواهیم داد.
مطالب
معرفی Lex.Db
Lex.Db یک بانک اطلاعاتی درون پروسه‌ای (مدفون شده یا embedded) بسیار سریع نوشته شده با سی‌شارپ است. این بانک اطلاعاتی کم حجم، سورس باز بوده و مجوز استفاده از آن LGPL است. به این معنا که استفاده از اسمبلی‌های آن در هر نوع پروژه‌ای آزاد است.
نکته مهم آن سازگاری با برنامه‌های دات نت 4 به بعد، همچنین برنامه‌های ویندوز 8، سیلورلایت 5، ویندوز فون 8 و همچنین اندروید (از طریق Mono) است. به علاوه چون با دات نت تهیه شده است، دیگر نیازی نیست دو نگارش 32 بیتی و 64 بیتی آن توزیع شوند و به این ترتیب مشکلات توزیع بانک‌های اطلاعاتی native مانند SQLite را ندارد ( و مطابق ادعای نویسنده آلمانی آن، از SQLite سریعتر است).
API این بانک اطلاعاتی، هر دو نوع متدهای synchronous  و  asynchronous را شامل می‌شود؛ به همین جهت با برنامه‌های ویندوز 8 و سیلورلایت نیز سازگاری دارد.
Lex.Db از برنامه‌های چندریسمانی و همچنین استفاده از یک بانک اطلاعاتی آن توسط چندین پروسه همزمان نیز پشتیبانی می‌کند.
در ادامه مروری خواهیم داشت بر نحوه استفاده از آن در حالت طراحی رابطه‌ای؛ از این جهت که فعلا به ظاهر این بانک اطلاعاتی روابط را پشتیبانی نمی‌کند، اما در عمل پیاده سازی آن مشکل نیست.

دریافت Lex.Db

برای دریافت Lex.Db، دستور ذیل را در خط فرمان پاورشل نیوگت وارد نمائید:
 PM> Install-Package Lex.Db
بسته به نوع پروژه شما (دات نت یا WinRT یا ...)، اسمبلی متناسبی به پروژه اضافه خواهد شد.


مدل‌های برنامه

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
    }

    public class Order
    {
        public int Id { get; set; }
        public int? CustomerFK { get; set; }
        public int[] ProductsFK { get; set; }
    }
مدل‌های برنامه آزمایشی مطلب جاری را در اینجا ملاحظه می‌کنید. برای طراحی روابط یک به صفر یا یک و همچنین یک به چند، تنها کافی است کلیدهای اصلی یا آرایه‌ای از کلیدهای اصلی مرتبط را در اینجا ذخیره کنیم، که نمونه‌ای از آن‌را در کلاس Order ملاحظه می‌کنید.


آغاز بانک اطلاعاتی

    public static class Database
    {
        public static DbInstance Instance { get; private set; }

        public static DbTable<Product> Products { get; private set; }
        public static DbTable<Order> Orders { get; private set; }
        public static DbTable<Customer> Customers { get; private set; }

        /// <summary>
        /// سازنده استاتیکی که در طول عمر برنامه فقط یکبار اجرا می‌شود
        /// </summary>
        static Database()
        {
            createDb();
            getTables();
        }

        private static void getTables()
        {
            Products = Instance.Table<Product>();
            Customers = Instance.Table<Customer>();
            Orders = Instance.Table<Order>();
        }

        private static void createDb()
        {
            Instance = new DbInstance(Path.Combine(Environment.CurrentDirectory, "LexDbTests"));

            Instance.Map<Product>()
                    .WithIndex("NameIdx", x => x.Name)
                    .Automap(i => i.Id, true);

            Instance.Map<Order>()
                    .Automap(i => i.Id, true);

            Instance.Map<Customer>()
                    .WithIndex("NameIdx", x => x.Name)
                    .WithIndex("CityIdx", x => x.City)
                    .Automap(i => i.Id, true);

            Instance.Initialize();
        }
    }
کلاس دیتابیس و سازنده آن، استاتیک تعریف شده‌اند؛ تا در طول عمر برنامه تنها یکبار وهله سازی شوند. new DbInstance یک وهله جدید از بانک اطلاعاتی را آغاز می‌کند. سازنده آن، مسیر پوشه‌ای که فایل‌های این بانک اطلاعاتی در آن ذخیره خواهند شد را دریافت می‌کند. Lex.Db به ازای هر کلاس مدلی که به آن معرفی شود، دو فایل data و index را ایجاد می‌کند.
سپس توسط وهله‌ای از بانک اطلاعاتی که ایجاد کردیم، کار معرفی خواص مدل‌های برنامه توسط متد Map و Automap انجام می‌شود. متد Automap خاصیت primary key کلاس را دریافت کرده و همچنین پارامتر دوم آن مشخص می‌کند که آیا این کلید اصلی به صورت خودکار ایجاد شود یا خیر. به علاوه در همینجا می‌توان روی فیلدهای مختلف، ایندکس نیز ایجاد کرد. متد WithIndex یک نام دلخواه را دریافت کرده و سپس خاصیتی را که باید بر روی آن ایندکس ایجاد شود، دریافت می‌کند.
در نهایت متد Initialize باید فراخوانی گردد. البته اگر برنامه شما WinRT است، این متد Initialize Async خواهد بود.
جداول نیز بر اساس مدل‌های برنامه از طریق متد Instance.Table در دسترس قرار گرفته‌اند.

افزودن اطلاعات به بانک اطلاعاتی
        private static void addData()
        {
            var customer1 = new Customer { Name = "customer1", City = "City1" };
            var customer2 = new Customer { Name = "customer2", City = "City2" };
            Database.Instance.Save(customer1, customer2); // automatic Id assignment after Save

            var product1 = new Product { Name = "product1" };
            var product2 = new Product { Name = "product2" };
            Database.Instance.Save(product1, product2); // automatic Id assignment after Save

            var order1 = new Order { CustomerFK = customer1.Id, ProductsFK = new[] { product1.Id } };
            var order2 = new Order { CustomerFK = customer2.Id, ProductsFK = new[] { product1.Id, product2.Id } };
            Database.Instance.Save(order1, order2); // automatic Id assignment after Save
        }
اکنون که کار آغاز بانک اطلاعاتی صورت گرفت، برای افزودن اطلاعات از متد Database.Instance.Save می‌توان استفاده کرد (در برنامه‌های WinRT از  متد Save Async استفاده کنید).
در اینجا نیازی به ذکر Id نمونه‌های ساخته شده نیست؛ از این جهت که در حین عملیات Save، به صورت خودکار انتساب خواهند یافت.
همچنین نحوه مقدار دهی کلیدهای خارجی نیز با استفاده از همین کلیدهای اصلی آماده شده است.


واکشی تمام اطلاعات

        private static void loadAll()
        {
            var orders = Database.Orders.LoadAll();
            foreach (var order in orders)
            {
                // نحوه دریافت اطلاعات مشتری بر اساس کلید خارجی ثبت شده
                var orderCustomer = Database.Customers.LoadByKey(order.CustomerFK.Value);
                Console.WriteLine("Order Id: {0}, Customer: {1} ({2}) {3}", order.Id, orderCustomer.Name, orderCustomer.Id, orderCustomer.City);

                // نحوه بازیابی لیستی از اشیاء مرتبط از طریق آرایه‌ای از کلیدهای خارجی ثبت شده
                var orderProducts = Database.Products.LoadByKeys(order.ProductsFK);
                foreach (var product in orderProducts)
                {
                    Console.WriteLine("  Product Id: {0}, Name: {1}", product.Id, product.Name);
                }
            }
        }
بانک اطلاعاتی آغاز شد؛ تعدادی رکورد نیز در آن ثبت گردید. اکنون برای بازیابی اطلاعات می‌توان از متدهای در دسترس جداول کلاس Database استفاده کرد. برای مثال متد LoadAll تمام رکوردهای یک جدول را واکشی می‌کند (در برنامه‌های WinRT این متد LoadAll Async خواهد بود).
سپس با استفاده از متدهای LoadByKey و LoadByKeys، به سادگی می‌توان اشیاء مرتبط با هر سفارش را نیز واکشی کرد.


استفاده از ایندکس‌ها برای کوئری گرفتن

        private static void queryingByAnIndex()
        {
            var name = "customer1";
            var customersList = Database.Customers
                                        .IndexQueryByKey("NameIdx", name)
                                        .ToList();
            foreach (var person in customersList)
            {
                Console.WriteLine(person.Name);
            }
        }
در ابتدای بحث، توسط متد WithIndex، تعدادی ایندکس را نیز تعریف کردیم. اکنون توسط این ایندکس‌ها و متد IndexQueryByKey، می‌توان کوئری‌هایی بسیار سریع را تهیه کرد.
            // Using Take and Skip
            var list1 = Database.Orders.Query<int>() // primary idx
                                       .Take(1).Skip(2).ToList();

            // Querying Between Ranges 
            var list2 = Database.Customers
                                .IndexQuery<string>("NameIdx")
                                .GreaterThan("a", orEqual: true).LessThan("d").ToList();
همچنین در اینجا متدهایی مانند Take و Skip و یا جستجو در یک بازه توسط متدهای GreaterThan و LessThan نیز پشتیبانی می‌شوند.


حذف رکوردها
        private static void deletingRecords()
        {
            Database.Customers.DeleteByKey(key: 1);

            var customers = Database.Customers.LoadByKeys(new[] { 1, 2 });
            Database.Customers.Delete(customers);
        }
برای حذف رکوردها از متدهای DeleteByKey و یا Delete می‌توان استفاده کرد. متد Delete می‌تواند آرایه‌ای از اشیاء را نیز قبول کند.
و اگر خواستید کل بانک اطلاعاتی را خالی کنید، متد Database.Instance.Purge اینکار را انجام خواهد داد.


کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید:
Program-LexDb.cs
 
مطالب
SortedSet در دات نت 4

SortedSet قرار گرفته در فضای نام System.Collections.Generic دات نت 4، لیستی از اشیاء به صورت خودکار مرتب شده را ارائه می‌دهد. SortedSet نیز همانند HashSet از اعضای منحصربفردی تشکیل خواهد شد اما اینبار به شکلی مرتب شده. برای پیاده سازی آن از red-black tree data structure استفاده شده است که مهم‌ترین مزیت آن امکان افزودن و یا حذف اشیاء به آن بدون کاهش قابل توجه کارآیی برنامه است.

مثال اول:
using System;
using System.Collections.Generic;

namespace SortedSetTest
{
class Program
{
static void sample1()
{
var setRange = new SortedSet<int> { 2, 5, 6, 2, 1, 4, 8 };

foreach (var i in setRange)
{
Console.WriteLine(i);
}
}

static void Main()
{
sample1();
}
}
}
در این مثال با نحوه‌ی ایجاد این لیست جنریک خود مرتب شونده‌ی تکراری نپذیر (!) آشنا می‌شوید. اگر این مثال را اجرا نمائید، خروجی آن مرتب شده است و همچنین تنها شامل یک عدد 2 است (اعضای تکراری را حذف می‌کند).

مثال دوم:

using System;
using System.Collections.Generic;

namespace SortedSetTest
{
class Program
{
static void sample2()
{
var setRange = new SortedSet<int>();
var random = new Random();

for (int counter = 0; counter < 100; counter++)
{
var rnd = random.Next(-180, 181);
if (!setRange.Add(rnd))
{
Console.WriteLine("Couldn't add {0}", rnd);
}
}

Console.WriteLine("Result set:");
foreach (var item in setRange)
{
Console.WriteLine(item);
}
}

static void Main()
{
sample2();
}
}
}
در این مثال نحوه‌ی افزودن اعضای مختلف به این لیست ویژه، توسط متد Add آن بیان شده است. اگر آیتمی در این لیست موجود باشد، مجددا اضافه نشده و حاصل متد Add آن، False خواهد بود.

مثال سوم:
اگر از سایر انواع سفارشی تعریف شده استفاده نمائید، باید روش مقایسه‌ی آن‌ها را نیز با پیاده سازی اینترفیس استاندارد IComparable ارائه دهید؛ در غیر اینصورت با خطای At least one object must implement IComparable متوقف خواهید شد.

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

namespace SortedSetTest
{
class FileInfo
{
public string Name { set; get; }
public long Size { set; get; }
}

class FileInfoComparer : IComparer<FileInfo>
{
public int Compare(FileInfo x, FileInfo y)
{
var caseiComp = new CaseInsensitiveComparer();
return caseiComp.Compare(x.Name, y.Name);
}
}


class Program
{

static void sample3()
{
var setRange = new SortedSet<FileInfo>(new FileInfoComparer())
{
new FileInfo
{
Name = "file1.txt",
Size = 100
},
new FileInfo
{
Name = "file2.txt",
Size = 10
},
new FileInfo
{
Name = "file3.txt",
Size = 300
}
};

foreach (var item in setRange)
{
Console.WriteLine(item.Name);
}
}

static void Main()
{
sample3();

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

در این مثال اشیایی از نوع کلاس FileInfo به لیست ویژه‌ی ما اضافه شده‌اند. برای اینکه امکان مقایسه‌ی آن‌ها فراهم باشد ، کلاس FileInfoComparer با پیاده سازی اینترفیس IComparer ، روش مقایسه دو شیء از این دست را ارائه می‌دهد.

مطالب
PowerShell 7.x - قسمت ششم - ایجاد Cmdletها توسط #C
تا اینجا با کمک توابع توانستیم PowerShell را به اصطلاح extend کنیم. نوع دیگر دستورات، command letها هستند. این نوع دستورات را با کمک یک زبان دات‌نتی میتوانیم ایجاد کنیم. به این نوع دستورات complied cmdlet گفته میشود. در بیشتر مواقع با کمک advanced functionها میتوانید بیشتر کارها را انجام دهید؛ چراکه به صورت مستقیم امکان استفاده از دات‌نت را درون PowerShell دارید. اما شاید ترجیح دهید از سی‌شارپ یا دیگر زبان‌ها دات‌نتی برای ایجاد یک تابع استفاده کنید.

نحوه‌ی ایجاد یک cmdlet با کمک #C
ابتدا یک دایرکتوری جدید را ایجاد کرده و درون آن یک پروژه‌ی از نوع class library را ایجاد کنید. سپس پکیج PowerShellStandard.Library را درون پروژه ایجاد شده با کمک dotnet cli به پروژه اضافه کنید: 
mkdir ps_cmdlet_with_csharp && cd "$_"
dotnet new classlib
dotnet add package PowerShellStandard.Library
mv Class1.cs GetHelloCommand.cs
در پایان دستورات فوق، نام فایل پیش‌فرض Class1 را نیز به GetHelloCommand تغییر داده‌ایم. در ادامه محتویات فایل را اینگونه ویرایش خواهیم کرد: 
namespace ps_cmdlet_with_csharp;
using System.Management.Automation;

[Cmdlet(VerbsCommon.Get, "Hello")]
public class GetHelloCommand : PSCmdlet
{
    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
    public string Name { get; set; }
    protected override void BeginProcessing()
    {
        WriteObject("Start processing");
    }
    protected override void ProcessRecord()
    {
        WriteObject("Hello " + Name);
    }
    protected override void EndProcessing()
    {
        WriteObject("End processing");
    }
}
همانطور که مشاهده میکنید، کلاس فوق را از PSCmdlet ارث‌بری کرده‌ایم. در کل برای ایجاد یک command let، از یکی از انواع Cmdlet یا PSCmdlet میتوانیم ارث‌بری کنیم. Cmdlet یک کلاس پایه است که PSCmdlet نیز از آن ارث برده است و یکسری امکانات بیشتری را جهت تعامل با PowerShell ارائه میدهد: 
using System.Collections.ObjectModel;
using System.Management.Automation.Host;

namespace System.Management.Automation
{
    public abstract class PSCmdlet : Cmdlet
    {
        protected PSCmdlet();

        public PSEventManager Events { get; }
        public PSHost Host { get; }
        public CommandInvocationIntrinsics InvokeCommand { get; }
        public ProviderIntrinsics InvokeProvider { get; }
        public JobManager JobManager { get; }
        public JobRepository JobRepository { get; }
        public InvocationInfo MyInvocation { get; }
        public PagingParameters PagingParameters { get; }
        public string ParameterSetName { get; }
        public SessionState SessionState { get; }

        public PathInfo CurrentProviderLocation(string providerId);
        public Collection<string> GetResolvedProviderPathFromPSPath(string path, out ProviderInfo provider);
        public string GetUnresolvedProviderPathFromPSPath(string path);
        public object GetVariableValue(string name);
        public object GetVariableValue(string name, object defaultValue);
    }
}
در نهایت command let باید به یک DLL تبدیل شود؛ چون همانطور که قبلآً نیز اشاره شد، هر command let در واقع یک شیء دات‌نتی است. در ادامه پروژه جاری را بیلد کرده و توسط دستور Import-Module فایل DLL تولید شده را درون Shell ایمپورت خواهیم کرد: 
PS /> dotnet build
PS /> Import-Module ./bin/Debug/net7.0/ps_cmdlet_with_csharp.dll
سپس توسط دستور Get-Command میتوانیم مطمئن شویم که ماژول موردنظر با موفقیت ایمپورت شده‌است: 
PS /> Get-Command -Module ps_cmdlet_with_csharp

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Cmdlet          Get-Hello                                          1.0.0.0    ps_cmdlet_with_csharp
همانطور که مشاهده میکنید، درون ماژول ps_cmdlet_with_csharp تنها یک command let تعریف شده‌است. درون یک namespace میتوانیم چندین command let را تعریف کنیم. به عنوان مثال برای مثال قبل میتوانیم: 
namespace ps_cmdlet_with_csharp;
using System.Management.Automation;

[Cmdlet(VerbsCommon.Get, "Hello")]
public class GetHelloCommand : PSCmdlet
{
    // as before
}

[Cmdlet(VerbsCommon.Get, "Greetings")]
public class GetGreetingsCommand : PSCmdlet
{
    [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
    public string Name { get; set; }
    protected override void BeginProcessing()
    {
        WriteObject("Start processing");
    }
    protected override void ProcessRecord()
    {
        WriteObject("Greetings " + Name);
    }
    protected override void EndProcessing()
    {
        WriteObject("End processing");
    }
}
اکنون اگر پروژه را بیلد کنیم و ماژول بیلد شده را ایمپورت کنیم، با خطای زیر مواجه خواهیم شد: 
Import-Module: Invalid assembly public key.
برای رفع این مشکل، فایل csproject را باز کرده و خط زیر را به آن اضافه کنید: 
<Project Sdk="Microsoft.NET.Sdk">

  <!-- other tags -->

  <ItemGroup>
    <Compile Include="./GetHelloCommand.cs" />
  </ItemGroup>

</Project>
اکنون پروژه با موفقیت بیلد و ایمپورت خواهد شد: 
PS /> Get-Command -Module ps_cmdlet_with_csharp

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Cmdlet          Get-Greetings                                      1.0.0.0    ps_cmdlet_with_csharp
Cmdlet          Get-Hello                                          1.0.0.0    ps_cmdlet_with_csharp

یک مثال تکمیلی
درون یک کلاس Cmdlet، امکان استفاده از تمامی annotationهایی را که در قسمت قبل بررسی کردیم، اینجا نیز در اختیار داریم؛ بنابراین نیاز به توضیح مجدد آن نیست. در ادامه میخواهیم یک دستور را با عنوان Push-SlackMessage تهیه کنیم که کار ارسال یک پیام را به یک کانال Slack، انجام میدهد: 
namespace ps_cmdlet_with_csharp;
using System.Management.Automation;
using System.Net.Http.Headers;


[Cmdlet(VerbsCommon.Push, "SlackMessage")]
[Alias("ssm")]
[OutputType(typeof(string))]
public class SlackMessageCommand : PSCmdlet
{
    [Parameter(Mandatory = true)]
    [Alias("m")]
    public string Message { get; set; }

    [Parameter(Mandatory = true)]
    [Alias("t")]
    [ValidatePattern(@"xoxp-[0-9]{11}-[0-9]{12}-[0-9]{13}-[0-9a-zA-Z]{32}")]
    public string Token { get; set; }

    [Parameter(Mandatory = true)]
    [Alias("cid")]
    public string ChannelID { get; set; }
    protected async override void ProcessRecord()
    {
        base.ProcessRecord();
        try
        {
            using var client = new HttpClient();
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", this.Token);
            var values = new Dictionary<string, string>
            {
                { "channel", this.ChannelID },
                { "text", this.Message }
            };
            var content = new FormUrlEncodedContent(values);
            var response = await client.PostAsync("https://slack.com/api/chat.postMessage", content);
            var responseString = await response.Content.ReadAsStringAsync();
            WriteObject("Message sent");
        }
        catch (Exception ex)
        {
            WriteObject(ex.Message);
        }
    }
}

یک نکته در مورد ایمپورت کردن ماژول‌ها
دستوراتی که تاکنون ایجاد کردیم (هم توابع و هم compiled cmletها) برای اجرا باید یکبار درون حافظه قرار بگیرند و سپس امکان اجرای آنها را خواهیم داشت. دلیل آن نیز این است که همه چیز درون سشن جاری انجام خواهد شد و به محض بستن آن، تغییرات نیز ار حافظه خارج خواهند شد. یعنی برای command letی که ایجاد کردیم، با هربار باز کردن یک سشن جدید مجبور خواهیم بود که مجدداً آن را ایمپورت کنیم. برای رفع این مشکل میتوانیم از پروفایل‌ها استفاده کنیم. توسط پروفایل، امکان سفارشی‌سازی شل را خواهیم داشت. پروفایل در واقع یک اسکریپت PowerShell است که به محض اجرای PowerShell فراخوانی خواهد شد. بنابراین درون پروفایل این فرصت را خواهیم داشت تا متغیرها، ماژول‌ها، aliaseها و… را قبل از باز کردن شل، درون سشن PowerShell بارگذاری کنیم. PowerShell از چندین نوع پروفایل پشتیبانی میکند و توسط متغیر خودکار PROFILE$ میتوانیم لیست مسیرهای پروفایل‌ها را مشاهده کنیم:  
PS /> $PROFILE | Get-Member -Type NoteProperty | Select-Object Name, Definitionbject Name, Definition

Name                   Definition
----                   ----------
AllUsersAllHosts       string AllUsersAllHosts=/usr/local/microsoft/powershell/7/…
AllUsersCurrentHost    string AllUsersCurrentHost=/usr/local/microsoft/powershell…
CurrentUserAllHosts    string CurrentUserAllHosts=/Users/sirwanafifi/.config/powe…
CurrentUserCurrentHost string CurrentUserCurrentHost=/Users/sirwanafifi/.config/p…
برای ویرایش هر کدام از پروفایل‌های موردنظر میتوانید اینگونه عمل کنید: 
$SlackProjectPath = "/Users/sirwanafifi/Desktop/ps_cmdlet_with_csharp/bin/Debug/net7.0/ps_cmdlet_with_csharp.dll"
Import-Module $SlackProjectPath
اکنون با هر بار باز کردن PowerShell به command let جدید دسترسی خواهید داشت: 
PS /> Get-Command -Module ps_cmdlet_with_csharp

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ----
Cmdlet          Push-SlackMessage                                  1.0.0.0    ps_…