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

همانطور که قول داده بودم، به اصول GRASP می‌پردازیم.

اصول GRASP-General Responsibility Assignment Software Principles

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

  • کنترلر ( Controller )
  • ایجاد کننده ( Creator )
  • انسجام قوی ( High Cohesion )
  • واسطه گری ( Indirection )
  • دانای اطلاعات ( Information Expert )
  • اتصال ضعیف ( Low Coupling )
  • چند ریختی ( Polymorphism )
  • حفاظت از تاثیر تغییرات ( Protected Variations )
  • مصنوع خالص ( Pure Fabrication )

 

Controller

این الگو بیان می‌کند که مسئولیت پاسخ به رویداد‌های (Events ) یک سناریوی محدود مانند یک مورد کاربردی ( Use Case ) باید به عهده یک کلاس غیر UI باشد. کنترلر باید کارهایی را که نیاز است در پاسخ رویداد انجام شود، به دیگران بسپرد و نتایج را طبق درخواست رویداد بازگرداند. در اصل، کنترلر دریافت کننده رویداد، راهنمای مسیر پردازش برای پاسخ به رویداد و در نهایت برگرداننده پاسخ به سمت مبداء رویداد است. در زیر مثالی را می‌بینیم که رویداد اتفاق افتاده توسط واسط گرافیکی به سمت یک handler (که متدی است با ورودیِ فرستنده و آرگمانهای مورد نیاز) در کنترلر فرستاده میشود. این روش event handling، در نمونه‌های وب فرم و ویندوز فرم دیده میشود. به صورتی خود کلاس‌های .Net وظیفه Event Raising از سمت UI با کلیک روی دکمه را انجام میدهد: 

 public class UserController
 {        
        protected void OnClickCreate(object sender, EventArgs e)
        {
           // call validation services
           // call create user services
        }
 }


در مثال بعد عملیات مربوط به User در یک WebApiController پاسخ داده میشود. در اینجا به جای استفاده از Event Raising برای کنترل کردن رویداد، از فراخوانی یک متد در کنترلر توسط درخواست HttpPost انجام میگیرد. در اینجا نیاز است که در سمت کلاینت درخواستی را ارسال کنیم:

    public class UserWebApiController
    {
        [HttpPost]
        public HttpResponseMessage Create(UserViewModel user)
        {
            // call validation services
            // call create user services
        }
    }



Creator :

  این اصل میگوید شیء ای میتواند یک شیء دیگر را بسازد ( instantiate ) که: (اگر کلاس B بخواهد کلاس A را instantiate کند)

  • کلاس B شیء از کلاس A را در خود داشته باشد؛
  • یا اطلاعات کافی برای instantiate کردن از A را داشته باشد؛
  • یا به صورت نزدیک با A در ارتباط باشد؛
  • یا بخواهد شیء A را ذخیره کند.

از آنجایی که این اصل بدیهی به نظر میرسد، با مثال نقض، درک بهتری را نسبت به آن میتوان پیدا کرد:

    // سازنده
    public class B
    {
        public static A CreateA(string name, string lastName, string job)
        {
            return new A() {
                Name =name,
                LastName = lastName,
                Job = job
            };
        }
    }
    // ایجاد شونده
    public class A
    {
        public string Name { get; set; }
        public string LastName { get; set; }
        public string Job { get; set; }
    }

    public class Context
    {
        public void Main()
        {
            var name = "Rasoul";
            var lastName = "Abbasi";
            var job = "Developer";            
            var obj = B.CreateA(name, lastName, job);
        }
    }


و اما چرا این مثال، اصل Creator را نقض میکند. در مثال میبینید که کلاس B، یک شیء از نوع A را در متد Main کلاس Context ایجاد میکند. کلاس B فقط یک متد برای تولید A دارد و در عملیات تولید A هیچ منطق خاصی را پیاده سازی نمیکند.کلاس B شیء ای را از کلاس A ، در خود ندارد، با آن ارتباط نزدیک ندارد و آنرا ذخیره نمیکند. با اینکه کلاس B اطلاعات کافی را برای تولید A از ورودی میگیرد، ولی این کلاس Context است که اطلاعات کافی را ارسال مینماید. اگر در کلاس B منطقی اضافه بر instance گیریِ ساده وجود داشت (مانند بررسی صحت و اعتبار سنجی)، میتوانستیم بگوییم کلاس B از یک مجموعه عملیات instance گیری با خبر است که کلاس Context  نباید از آن خبر داشته باشد. لذا اکنون هیچ دلیلی وجود ندارد که وظیفه تولید A را در Context انجام ندهیم و این مسئولیت را به کلاس B منتقل کنیم. این مورد ممکن است در ذهن شما با الگوی Factory تناقض داشته باشد. ولی نکته اصلی در الگو Factory انجام عملیات instance گیری با توجه به منطق برنامه است؛ یعنی وظیفه‌ای که کلاس Context نباید از آن خبر داشته باشد را به کلاس Factory منتقل میکنیم. در غیر اینصورت ایجاد کلاس Factory بی معنا خواهد بود (مگر به عنوان افزایش انعطاف پذیری معماری که بتوان به راحتی نوع پیاده سازی یک واسط را تغییر داد).


High Cohesion :

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

در مثال زیر نقض این اصل را مشاهده میکنیم:

    class Controller
    {
        public void CreateProduct(string name, int categoryId) { }
        public void EditProduct(int id, string name) { }
        public void DeleteProduct(int id) { }
        public void CreateCategory(string name) { }
        public void EditCategory(int id, string name) { }
        public void DeleteCategory(int id) { }
    }  

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


Indirection :

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

الگوهای Adapter و Delegate و همچنین نقش کنترلر در الگوی معماری MVC از این اصل پیروی میکنند. 

    class SenderA
    {
        public Mediator mediator { get; }
        public SenderA() { mediator = new Mediator(); }
        public void Send(string message, string reciever) { mediator.Send(message, reciever); }
    }
    class SenderB
    {
        public Mediator mediator { get; }
        public SenderB() { mediator = new Mediator(); }
        public void Send(string message) { }
    }

    public class RecieverA
    {
        public void DoAction(string message)
        {
            // انجام عملیات بر اساس پیغام دریافت شده
            switch (message)
            {
                case "create":
                    break;
                case "delete":
                    break;
                default:
                    break;
            }
        }
    }
    public class RecieverB
    {
        public void DoAction(string message)
        {
            // انجام عملیات بر اساس پیغام دریافت شده
            switch (message)
            {
                case "edit":
                    break;
                case "rollback":
                    break;
                default:
                    break;
            }
        }
    }
    class Mediator
    {
        internal void Send(string message, string reciever)
        {
            switch (reciever)
            {
                case "A":
                    var recieverObjA = new RecieverA();
                    recieverObjA.DoAction(message);
                    break;
                case "B":
                    var recieverObjB = new RecieverB();
                    recieverObjB.DoAction(message);
                    break;

                default:
                    break;
            }
        }
    }
    class IndirectionContext
    {
        public void Main()
        {
            var senderA = new SenderA();
            senderA.Send("rollback", "B");
            var senderB = new SenderA();
            senderB.Send("create", "A");

        }
    }

در این مثال کلاس Mediator به عنوان واسط ارتباطی بین کلاس‌های Sender و Receiver قرار گرفته و نقش تحویل پیغام را دارد.

در مقاله بعدی، به بررسی سایر اصول GRASP خواهم پرداخت.

بازخوردهای دوره
افزونه‌ای برای کپسوله سازی نکات ارسال یک فرم ASP.NET MVC به سرور توسط jQuery Ajax
با سلام
من هم دقیقا همین رو نوشتم ولی بعد از اینکه alert ظاهر میشه نتیجه این است json data: Undifined
آیا کار دیگری هم باید انجام داد؟

و اینکه زمانی که در پارشال ویو از آن استفاده می‌شود یک صفحه سفید نشان می‌دهد که داخل آن {"result":"ok"} را نوشته است.
با تشکر
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت ششم
در این قسمت مدل‌های باقی مانده‌ی از بخش‌هایی را که در مقاله اول مطرح شدند، به اتمام می‌رسانیم. همچنین با بازخوردهایی که در مقالات قبل گرفتیم، در این قسمت تغییرات ایجاد شده‌ی در مدل‌های قسمت‌های قبل را نیز مطرح خواهیم کرد.

مدل‌های AuditLog (اصلاحیه)و ActivityLog

باید توجه داشت که اگر سیستم AuditLog، جزئیات بیشتری را در بر بگیرد، می‌توان از آن به عنوان History هم یاد کرد. در قسمت چهارم برای پست‌های انجمن یک جدول جدا هم به منظور ذخیره سازی تاریخچه‌ی تغییرات، در نظر گرفتیم. فرض کنید که یک سری از جداول دیگر هم نیازمند این امکان باشند! راه حل چیست؟
  1. استفاده از جداول جدا برای هر کدام از جداول به صورتیکه یک ارتباط یک به چند مابین آنها برقرار است. از این جداول تحت عنوان HistoryTable یاد می‌شود.
  2. استفاده از یک جدول برای نگهداری تاریخچه‌ی تغییرات جداولی که نیازمند این امکان هستند. 
در زیر پیاده سازی از روش دوم رو مشاهده میکنید.
  /// <summary>
    /// Represent The Operation's log
    /// </summary>
    public class AuditLog
    {
        #region Ctor
        /// <summary>
        /// Create One Instance Of <see cref="AuditLog"/>
        /// </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>
        /// gets or sets Type of  Modification(create,softDelet,Delete,update)
        /// </summary>
        public virtual AuditAction Action { 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 Type Of Entity 
        /// </summary>
        public virtual string Entity { get; set; }
        /// <summary>
        /// gets or sets  Old value of  Properties before modification
        /// </summary>
        public virtual string XmlOldValue { get; set; }
        /// <summary>
        /// gets or sets XML Base OldValue of Properties (NotMapped)
        /// </summary>
        public virtual XElement XmlOldValueWrapper
        {
            get { return XElement.Parse(XmlOldValue); }
            set { XmlOldValue = value.ToString(); }
        }
        /// <summary>
        /// gets or sets new value of  Properties after modification
        /// </summary>
        public virtual string XmlNewValue { get; set; }
        /// <summary>
        /// gets or sets XML Base NewValue of Properties (NotMapped)
        /// </summary>
        public virtual XElement XmlNewValueWrapper
        {
            get { return XElement.Parse(XmlNewValue); }
            set { XmlNewValue = value.ToString(); }
        }
        /// <summary>
        /// gets or sets Identifier Of Entity
        /// </summary>
        public virtual string EntityId { get; set; }
        /// <summary>
        /// gets or sets user agent information
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets user's ip address
        /// </summary>
        public virtual string OperantIp { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets log's creator
        /// </summary>
        public virtual User Operant { get; set; }
        /// <summary>
        /// sets or gets identifier of log's creator
        /// </summary>
        public virtual long OperantId { get; set; }
        #endregion
    }
  public enum AuditAction
    {
        Create,
        Update,
        Delete,
        SoftDelete,
    }

خصوصیاتی که نیاز به توضیح خواهند داشت:
  • Action : از نوع AdutiAction است و برای مشخص کردن نوع عملیاتی که انجام شده است، می‌باشد.
  • Description : اگر نیاز باشد توضیحاتی اضافی ثبت شوند، از این خصوصیت استفاده می‌شود.
  • Entity : مشخص کننده‌ی نام مدل خواهد بود. شاید بهتر بود از یک Enum استفاده می‌شد. ولی این سیستم به احتمال زیاد قرار است افزونه پذیر باشد و استفاده از Enum، یعنی محدودیت و این امکان وجود نخواهد داشت که سایر افزونه‌ها بتوانند از مدل بالا استفاده کنند. برا ی مثال BlogPost , NewsItem , ForumPost , ...
  • EntitytId : آی دی رکوردی است که تاریخچه‌ی آن ثبت شده است. از آنجائیکه بعضی از موجودیت‌ها دارای آی دی از نوع long و برخی دیگر Guid ، لذا ذخیره‌ی رشته‌ای آن مفید خواهد بود.
  • XmlOldValue : در برگیرنده‌ی مقدار (قبل از اعمال تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشته‌ای ذخیره شوند.
  • XmlNewValue : در برگیرنده‌ی مقدار (بعد از تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشته‌ای ذخیره شوند.
  • Operant, OperantId: برای برقراری ارتباط یک به چند مابین مدل کاربر و مدل بالا در نظر گرفته شده‌اند که به عنوان انجام دهنده‌ی این تغییرات بوده است.
با استفاده از مدل بالا می‌توان متوجه شد که کاربر x چه خصوصیاتی از  موجودیت y را تغییر داده است و این خصوصیات قبل از تغییر چه مقدارهایی داشته‌اند.
  /// <summary>
    /// Represents Activity Log record
    /// </summary>
    public class ActivityLog
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="ActivityLog"/>
        /// </summary>
        public ActivityLog()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            OperatedOn=DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier 
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets the comment of this activity
        /// </summary>
        public virtual string Comment { get; set; }
        /// <summary>
        /// gets or sets the date that this activity was done
        /// </summary>
        public virtual DateTime OperatedOn { get; set; }
        /// <summary>
        /// gets or sets the page url . 
        /// </summary>
        public virtual string Url { get; set; }
        /// <summary>
        /// gets or sets the title of page if Url is Not null
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets user agent information
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets user's ip address
        /// </summary>
        public virtual string OperantIp { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets the type of this activity
        /// </summary>
        public virtual ActivityLogType Type{ get; set; }
        /// <summary>
        /// gets or sets the  type's id of this activity
        /// </summary>
        public virtual Guid TypeId { get; set; }
        /// <summary>
        /// gets or sets User that done this activity
        /// </summary>
        public virtual User Operant { get; set; }
        /// <summary>
        /// gets or sets Id of User that done this activity
        /// </summary>
        public virtual long OperantId { get; set; }
        #endregion
    }

   /// <summary>
    /// Represents Activity Log Type Record
    /// </summary>
    public class ActivityLogType
    {
        #region Ctor
        /// <summary>
        /// Create one Instance of <see cref="ActivityLogType"/>
        /// </summary>
        public ActivityLogType()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier 
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets the system name
        /// </summary>
        public virtual string Name{ get; set; }
        /// <summary>
        /// gets or sets the display name
        /// </summary>
        public virtual string DisplayName { get; set; }
        /// <summary>
        /// gets or sets the description 
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// indicate this log type is enable for logging
        /// </summary>
        public virtual bool IsEnabled { get; set; }
        #endregion
    }
مدل‌های بالا هم برای ثبت لاگ فعالیت‌های کاربران در سیستم در نظر گرفته شده است . برای مثال اگر بخش آخرین تغییرات سایت جاری را هم مشاهده کنید، یک همچین سیستمی را هم دارد. این لاگ‌ها برای ردیابی عملکرد کاربران در سیستم مفید خواهد بود.
  • Comment : توضیحات کوتاهی از اکشنی که کاربر انجام داده است.
  • Url : آدرس صفحه‌ای که این عملیات در آنجا انجام شده است. این خصوصیت نال‌پذیر می‌باشد.
  • Title : عنوان صفحه‌ای که این عملیات در آنجا انجام شده است؛ اگر Url نال نباشد.
  • Operant , OperantId : برای برقراری ارتباط یک به چند بین کاربر و مدل فعالیت‌ها در نظر گرفته شده‌اند.
  • Type : از نوع ActivityLogType پیاده سازی شده در بالا می‌باشد. با استفاده از مدل ActivityLogType می‌توان مثلا لاگ فعالیت مربوط به بخش اخبار را غیر فعال کند یا بالعکس و از این موارد.
خصوصیات مدل ActivityLogType :
  • Name : نام سیستمی آن است. برای مثال : Login ، NewsComment ، NewsItem و ...
  • IsEnabled : نشان دهنده‌ی این است که این نوع لاگ فعال است یا خیر.

کلاس پایه تمام مدل‌ها (اصلاحیه)

/// <summary>
    /// Represents the  entity
    /// </summary>
    /// <typeparam name="TForeignKey">type of user's Id that can be long or long? </typeparam>
    public abstract class Entity<TForeignKey>
    {
        #region Properties
        /// <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>
        /// gets or sets IP Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or set IP Address of Modifier
        /// </summary>
        public virtual string ModifierIp { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// indicate this entity is deleted softly
        /// </summary>
        public virtual bool IsDeleted { get; set; }
        /// <summary>
        /// gets or sets information of user agent of modifier
        /// </summary>
        public virtual string ModifierAgent { get; set; }
        /// <summary>
        /// gets or sets information of user agent of Creator
        /// </summary>
        public virtual string CreatorAgent { 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 count of Modification Default is 1
        /// </summary>
        public virtual int Version { get; set; }
        /// <summary>
        /// gets or sets action (create,update,softDelete) 
        /// </summary>
        public virtual AuditAction Action { 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 TForeignKey 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 TForeignKey CreatedById { get; set; }
        #endregion
    }

  /// <summary>
    /// Represents the base Entity
    /// </summary>
    /// <typeparam name="TKey">type of Id</typeparam>
    /// <typeparam name="TForeignKey">type of User's Id that can be long or long?</typeparam>
    public abstract class BaseEntity<TKey,TForeignKey> : Entity<TForeignKey>
    {
        #region Properties
        /// <summary>
        /// gets or sets Identifier of this Entity
        /// </summary>
        public virtual TKey Id { get; set; }
        #endregion
    }
از دو کلاس معرفی شده‌ی در بالا برای کپسوله کردن یکسری خصوصیات تکراری استفاده شده است. البته با بهبودهایی نسبت به مقاله‌ی قبل که با مشاهده‌ی خصوصیات آنها قابل فهم خواهد بود. در برخی از مدل‌ها، برای مثال نظرات وبلاگ امکان ارسال نظر برای افراد Anonymous هم وجود داشت؛ لذا CreatedById امکان نال بودن را هم داشت. به همین دلیل برای کاهش کدها، کلاس‌های بالا را به صورت جنریک تعریف کردیم. فایل EDMX نهایی که در انتهای مقاله ضمیمه شده، برای درک تغییرات اعمال شده مفید خواهد.

مدل سیستم آگاه سازی

/// <summary>
    /// Represents the Notification Record
    /// </summary>
    public class Notification
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="Notification"/>
        /// </summary>
        public Notification()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            ReceivedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// indicate that this notification is read by owner
        /// </summary>
        public virtual bool IsRead { get; set; }
        /// <summary>
        /// gets or sets notification's text body
        /// </summary>
        public virtual string Message { get; set; }
        /// <summary>
        /// gets or sets page url that this notification is related with it
        /// </summary>
        public virtual string Url { get; set; }
        /// <summary>
        /// gets or sets date that this Notification Received
        /// </summary>
        public virtual DateTime ReceivedOn { get; set; }
        /// <summary>
        /// gets or sets the type of notification
        /// </summary>
        public virtual NotificationType Type { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets the id of user that is owner of this notification
        /// </summary>
        public virtual long OwnerId { get; set; }
        /// <summary>
        /// gets or sets the user that is owner of this notification
        /// </summary>
        public virtual User Owner { get; set; }
        #endregion
    }

    public enum  NotificationType
    {
        NewConversation,
        NewConversationReply,
        ...
    }
در این سیستم برای اطلاع رسانی کاربر، علاوه بر ارسال ایمیل، بحث اطلاع رسانی RealTime را هم خواهیم داشت. اطلاع رسانی‌هایی که توسط کاربر خوانده نشده باشند، در جدول حاصل از مدل Notification ذخیره خواهند شد. خصوصیاتی که نیاز به توضیح دارند:
  • IsRead : مشخص کننده‌ی این است که یک اطلاع رسانی خوانده شده است یا خیر. در آخر هر روز اطلاع رسانی‌هایی که دارای خصوصیت IsRead با مقدار true هستند، حذف خواهند شد.
  • Url : برای مواردی که لازم است کاربر با کلیک بر روی آن به صفحه‌ی خاصی هدایت شود.
  • OwnerId, Owner : برای برقراری ارتباط یک به چند بین کاربر و مدل Notification در نظر گرفته شده‌اند.
  • Type : از نوع NotificationType و مشخص کننده‌ی نوع اطلاع رسانی می‌باشد .

مدل Observation

 public class Observation
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="Observation"/>
        /// </summary>
        public Observation()
        {
            LastObservedOn = DateTime.Now;
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets datetime of last visit 
        /// </summary>
        public virtual DateTime LastObservedOn { get; set; }
        /// <summary>
        /// gets or sets Id Of section That user is  observing the entity
        /// </summary>
        public virtual string SectionId { get; set; }
        /// <summary>
        /// gets or sets  section That user is  observing in it
        /// </summary>
        public virtual string Section { get; set; }
        #endregion

        #region NavigationProperites
        /// <summary>
        /// gets or sets user that observed the entity
        /// </summary>
        public virtual User Observer { get; set; }
        /// <summary>
        /// gets or sets identifier of user that observed the entity
        /// </summary>
        public virtual long ObserverId { get; set; }
        #endregion
    }
این مدل برای ایجاد امکانی به منظور واکشی لیست افردای که در حال مشاهده‌ی یک بخش خاص هستند، مفید است. فرض کنید در یک انجمن قصد دارید لیست افردای را که در حال مشاهده‌ی آن هستند، در پایین صفحه نمایش دهید. برای تاپیک‌ها هم همین امکان لازم است. لذا مدل بالا مختص مدل خاصی نیست و برای هر بخشی می‌توان از آن استفاده کرد. 
فرض کنیم کاربری قصد هدایت به یک تاپیک را دارد. لذا هنگام هدایت شدن لازم است رکوردی در جدول حاصل از مدل بالا ثبت شود که کاربر x در بخش Topic، در تاریخ d، تاپیک به شماره‌ی y را مشاهده کرد. در صفحه‌ی مشاهده‌ی تاپیک می‌توان لیست افرادی را که قبل از مدت زمان مشخصی تاپیک را مشاهده کرده اند، نمایش داد. این رکورد‌ها را هم با تعریف یک Task می‌توان در بازه‌های زمانی مشخصی حذف کرد.

مدل صفحات داینامیک

/// <summary>
    /// represents one custom page
    /// </summary>
    public class Page : BaseEntity<long, long>
    {
        #region Properties
        /// <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 SlugUrl { get; set; }
        /// <summary>
        /// gets or sets meta title for seo
        /// </summary>
        public virtual string MetaTitle { get; set; }
        /// <summary>
        /// gets or sets meta keywords for seo
        /// </summary>
        public virtual string MetaKeywords { get; set; }
        /// <summary>
        /// gets or sets meta description of the content
        /// </summary>
        public virtual string MetaDescription { get; set; }
        /// <summary>
        /// gets or sets 
        /// </summary>
        public virtual string FocusKeyword { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content use CanonicalUrl
        /// </summary>
        public virtual bool UseCanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets CanonicalUrl That the Post Point to it
        /// </summary>
        public virtual string CanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Follow for Seo
        /// </summary>
        public virtual bool UseNoFollow { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Index for Seo
        /// </summary>
        public virtual bool UseNoIndex { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content in sitemap
        /// </summary>
        public virtual bool IsInSitemap { get; set; }
        /// <summary>
        /// gets or sets title for snippet
        /// </summary>
        public string SocialSnippetTitle { get; set; }
        /// <summary>
        /// gets or sets description for snippet
        /// </summary>
        public string SocialSnippetDescription { get; set; }
        /// <summary>
        /// gets or sets section's type that this page show on
        /// </summary>
        public virtual ShowPageSection Section { get; set; }
        /// <summary>
        /// indicate this page has not any body
        /// </summary>
        public virtual bool IsCategory { get; set; }
        /// <summary>
        /// gets or sets order for display forum
        /// </summary>
        public virtual int DisplayOrder { get; set; }

        #endregion

        #region NavigationProeprties
        /// <summary>
        /// gets or sets Parent of this page
        /// </summary>
        public virtual Page Parent { get; set; }
        /// <summary>
        /// gets or sets parent'id of this page
        /// </summary>
        public virtual long? ParentId { get; set; }
        /// <summary>
        /// get or set collection of page that they are children of this page
        /// </summary>
        public virtual ICollection<Page> Children { get; set; }
        #endregion
    }

  public enum ShowPageSection
    {
        Menu,
        Footer,
        SideBar
    }
مدل بالا مشخص کننده‌ی صفحاتی است که مدیر می‌تواند در پنل مدیریتی آنها را برای استفاده‌های خاصی تعریف کند. حالت درختی آن مشخص است. یکسری از خصوصیات مربوط به محتوای صفحه و همچنین تنظیمات سئو برای آن در نظر گرفته شده است که بیشتر آنها در مقالات قبل توضیح داده شده‌اند. خصوصیت Section از نوع ShowPageSection و برای مشخص کردن امکان نمایش صفحه‌ی مورد نظر در نظر گرفته شده‌است. همچنین این مدل بالا از کلاس پایه‌ی مطرح شده‌ی در اول مقاله، ارث بری کرده است که امکان ردیابی تغییرات آن را مهیا می‌کند.
  خوب! حجم مقاله زیاد شده است و تا اینجا کافی خواهد بود ؛ بر خلاف تصور بنده، یک مقاله‌ی دیگر نیز برای اتمام بحث لازم میباشد.

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

نظرات مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
سلام من در ویو خودم نمیتونم اطلاعاتم رو تو kendo.grid  ببینم و برای من یک لیست استرینگ در ویو نمایش داده میشه و به این شکل در کنترلر و ویو کد نویسی کردم .
public class EFController : Controller    {
        //
        // GET: /EF/
 
 
        public ActionResult AjaxConnected([DataSourceRequest] DataSourceRequest request )
        {
            using (var dbef=new dbTestEntities())
            {
                IQueryable<Person> persons = dbef.People;
                DataSourceResult result = persons.ToDataSourceResult(request);
                return Json(result.Data,JsonRequestBehavior.AllowGet);
            }
        }
 
 
    }
و ویو 
@{    ViewBag.Title = "AjaxConnected";
}
 
 
<h2>AjaxConnected</h2>
@(Html.Kendo().Grid<TelerikMvcApp2.Models.Person>(  )
      .Name("Grid")
      .DataSource(builder => builder
          .Ajax()
          .Read(operationBuilder => operationBuilder.Action("AjaxConnected", "EF"))
 
 
      )
      .Columns(factory =>
      {
          factory.Bound(person => person.personId);
          factory.Bound(person => person.Name);
          factory.Bound(person => person.LastName);
      })
      .Pageable()
      .Sortable())
و یک لیست استرینگ بهم در عمل خروجی میده و از خود قالب kendogrid خبری نیست . من اطلاعات رو به طور json پاس میدم و ajaxi میگیرم. 
حالا قبلش همچین خطلایی داشتم که به allowget ایراد میگرفت ولی در کل با JsonRequestBehavior.AllowGet  حل شد  و  حالا فقط یه لسیت بهم خروجی میده! و از ظاهر گرید خبری نیست. و اگر به جای json نوشته بشه view و با ویو return کنم ظاهر kendogrid رو دارم اما خروجی دارای مقداری نیست! 
اینم خروجی استرینگ من :(
[{"personId":1,"Name":"Amin","LastName":"Saadati"},  {"personId":2,"Name":"Fariba","LastName":"Ghochani  "},{"personId":4,"Name":"Milad","LastName":"Rahman  i"},{"personId":5,"Name":"rima","LastName":"rad"},  {"personId":6,"Name":"ali","LastName":"kiva"},{"pe  rsonId":7,"Name":"sahel","LastName":"abasi"},{"per  sonId":8,"Name":"medi","LastName":"ghaem"},{"perso  nId":9,"Name":"mino","LastName":"kafash"},{"person  Id":10,"Name":"behzad","LastName":"tizro"},{"perso  nId":11,"Name":"toti","LastName":"saadati"},{"pers  onId":12,"Name":"parinaz","LastName":"karami"},{"p  ersonId":13,"Name":"sadegh","LastName":"hojati"},{  "personId":14,"Name":"milad","LastName":"ebadipor"  },{"personId":15,"Name":"farid","LastName":"riazi"  },{"personId":16,"Name":"said","LastName":"abdoli"  },{"personId":17,"Name":"behzad","LastName":"ariaf  ar"},{"personId":18,"Name":"jamshid","LastName":"k  otahi"}]
این سوال رو در چند سایت پرسیدم و به جوابی برایش نرسیدم. و نمیدونم ایراد کد‌های نوشته شده ام کجاست!
متشکرم
بازخوردهای دوره
انتقال خودکار Data Annotations از مدل‌ها به ViewModelهای ASP.NET MVC به کمک AutoMapper
اصل سخن بنده این است که با وجود ویو مدل ذکر متن اعتبار سنجی در خود Domain Class ضرورتی ندارد چون در نهایت ViewModel ما در View مورد استفاده قرار خواهد گرفت . من کل مطالبی که فرمودید را قبول دارم فقط اگر ذکر پیغام اعتبار سنجی‌ها  در Domain Class برای دست یابی به  هدف مقاله ذکر شده باشد وگر نه چه ضرورتی دارد پیغام خطا را در مدلی که در ویو استفاده نخواهد شد ذکر کنیم . 
با تشکر
مطالب
انتقال SVN به یک سیستم جدید

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

الف) دریافت و نصب Visual SVN server
یا می‌توان SVN خالص را از سایت آن دریافت کرد و یا جهت سهولت کار و همچنین دسترسی به یک کنسول مدیریتی می‌توان برنامه‌ی رایگان Visual SVN server را از آدرس زیر دریافت و نصب کرد:

پس از نصب، ابتدا باید یا کاربر جدیدی را جهت استفاده از منابع آن تعریف کرد و یا از نحوه‌ی اعتبار سنجی یکپارچه با ویندوز هم می‌توان استفاده کرد که من از این روش دوم استفاده می‌کنم (شکل زیر، کلیک راست بر روی نود اصلی visual SVN server و سپس انتخاب خواص و مراجعه به برگه‌ی اعتبار سنجی آن):



ب)دریافت و نصب TortoiseSVN

نصب آن نکته‌ی خاصی ندارد. اما یک سری نکته‌ی ریز پس از نصب آن بهتر است رعایت شود که در ادامه ذکر می‌شود:

ج) دریافت و نصب برنامه‌ی WinMerge
برنامه‌ی Diff پیش فرض TortoiseSVN آنچنان قوی نیست. به همین جهت می‌توان برنامه‌ی WinMerge را با آن یکپارچه کرد. برای این منظور ابتدا آن‌را دریافت نمائید:

اگر پس از نصب TortoiseSVN آن‌را نصب کنید، در حین نصب پیشنهاد یکپارچه سازی با TortoiseSVN را نیز می‌دهد. اگر ابتدا WinMerge را نصب کرده‌اید و سپس TortoiseSVN بر روی سیستم شما نصب شده، فقط کافی است مطابق شکل زیر ابتدا به قسمت Diff viewers آن مراجعه کرده و سپس با انتخاب گزینه‌ی external ، دستور خط فرمان زیر را وارد نمائید:

C:\Program Files (x86)\WinMerge\WinMergeU.exe -e -x -ub -dl %bname -dr %yname %base %mine



بدیهی است مسیر WinMergeU.exe مطابق مسیر نصب در سیستم شما باید تنظیم شود.

د) تنظیم مسیر تحت نظر قرار گرفتن سیستم
TortoiseSVN به صورت پیش فرض کل سیستم را جهت مشاهده‌ی تغییرات تحت نظر قرار می‌دهد که گاهی باعث کاهش کارآیی آن خواهد شد. برای رفع این مشکل می‌توان مسیرهایی را که پروژه‌های شما در آن قرار دارند را به آن معرفی نمود تا بار کلی سیستم کاهش یابد.



همانطور که در شکل نیز ملاحظه می‌کنید، Include path مقدار دهی شده است.

ه) مشخص سازی پسوندهایی که بهتر است از آن‌ها صرفنظر شود
به برگه‌ی general تنظیمات TortoiseSVN مراجعه کرده و در قسمت global ignore pattern آن، موارد زیر را وارد نمائید:



این موارد شامل پروژه‌های دات نت، دلفی ، VC و امثال آن است و همچنین یک سری فایل بایناری که عموما با پروژه‌های برنامه نویسی نیازی به ثبت نگارش آن‌ها نیست.

*.dcu *.~* dcu temp *.exe *.zip *.bkm *.ddp *.cfg *.dof *.dsk *.ini *.hlp *.gid *.bmp *.png *.gif ~* *.log bin debug release *.map *.chm *.bkf Thumbs.db *.mdb .obj *.elf *.stat *.ddp *.bpl *.map *.GID *.hlp *.opt *.dll *.raw *.BIN *.obj *.pdb *.scc Debug Release *.xml obj *.~* *.backup *.INI *.ArmLog *.KeyLog *.NanoLog *.Stats *.PreARM *.old *.drc *.*~ *.doc *.pdf *.bmp *.jpg *.MRW *.NEF *.ORF *.psd *.X3F __history *.local *.identcache *.bak Thumbs.db *.ldb *.dex *.rar DllDcu *.lck CVS cvs *.txt *.TXT *.jdbg *.HLP *.KWF *.xls *.cnt *.dsm *.dti *.tmp *.lnk *.cbk *.mes *.suo *.ncb *.user _ReSharper.* [Bb]in obj [Dd]ebug [Rr]elease *.aps *.eto


در همین برگه، اگر هنوز از VS2003 استفاده می‌کنید، تیک مربوط به استفاده از _svn بجای .svn را قرار دهید تا VS.Net با پوشه‌های مدیریتی ذکر شده مشکل پیدا نکند.

و) نصب افزونه‌های SVN سازگار با VS.Net
یا می‌توان از افزونه‌ی Visual SVN استفاده کرد (که رایگان نیست) و یا AnkhSVN که رایگان و سورس باز است.
ولی در کل یک مورد را بیشتر نصب نکنید. علت هم کند شدن VS.Net است به دلیل فعالیت‌های پشت صحنه‌ی هر کدام از این افزونه‌ها که زیاده روی در تعداد آن‌ها گاها باعث کرش هم می‌شود. بنابراین همان یک مورد کافی است.

ز) Import مخزن‌های قبلی
تا اینجا مقدمات کار فراهم شد. اکنون نوبت به import مخزن‌های بجا مانده از سیستم قبلی است. برای اینکار مطابق شکل زیر، گزینه‌ی import existing repositories را انتخاب کرده و مسیر مخزن‌های قبلی خود را باید معرفی نمود (به ازای هر کدام یکبار باید این عملیات صورت گیرد).



پس از انجام این مراحل یکبار باید سیستم reboot شود و اکنون همه چیز مثل قبل خواهد شد!


نکته:
اگر مسیر ریشه مخزن‌های جدید با مسیر آن‌ها در سیستم قبلی متفاوت است، هنگام commit کارهای خود با خطای زیر متوقف خواهید شد:
Commit failed (details follow): Unable to open an ra_local session to URL
Unable to open repository 'file:///C:/Repositories/tracking/trunk'




اشکالی ندارد! برای رفع آن باید از گزینه‌ی relocate مربوط به TortoiseSVN استفاده کرد.
بر روی پوشه کاری پروژه خود کلیک راست کرده، انتخاب گزینه‌ی TortoiseSVN و سپس انتخاب گزینه‌ی Relocate آن باید صورت گیرد. در اینجا می‌توان مسیر جدید ریشه اصلی مخزن را در سیستم جدید معرفی کرد.



مطالب
آپلود فایل ها با استفاده از PlUpload در Asp.Net Mvc
امروزه بازار برنامه‌های تماما ajax و بدون Postback  شدن صفحه بسیار داغ میباشد که از این موارد میتوان به برنامه‌های تحت وب گوگل اشاره کرد. (gmail  ، googlePlus  ، Google Reader)
در این میان یکی از دغدغه‌های توسعه دهندگان وب ، آپلود فایل‌ها به صورت آنی (مثل attach files گوگل) میباشد. برای حل این مسئله ، ابزارها و پلاگین‌های متعددی وجود دارد که در اینجا به 10 تا از پلاگین‌های Jquery  اشاره شده است.
به شخصه با پلاگین Uploadify کار کرده ام و از استفاده از آن راضی هستم ولی همین دیشب برای قسمتی از یک پروژه نیاز
به ابزاری جهت آپلود فایل‌ها با امکانات مورد نظرم داشتم که به PlUpload برخورد کردم. 

از امکاناتی که این ابزار در اختیار شما قرار میدهد :
- یک اینترفیس زیبا جهت آپلود و افزودن فایل ها
- پشتیبانی از زبان‌های مختلف و همین طور زبان فارسی
- امکان استفاده از قالب Jquery UI
- Drag&Drop  برای مرورگرهایی که از Html5  پشتیبانی میکنند

حال که با امکانات این ابزار بیشتر آشنا شدید بریم سراغ استفاده از این ابزار در asp.net mvc  :)
ابتدا پروژه را از اینجا دانلود کنید. سپس یک پروژه‌ی جدید  mvc 3  بسازید (از نوع Internet Application و با نام دلخواه). سپس پوشه‌ی plupload  را در قسمت سلوشن برنامه کپی کنید.
حال در فایل Views->Shared->_Layout.cshtml  ، تگ head  را جهت افزودن امکانات پلاگین این گونه تغییر دهید :

    <title>@ViewBag.Title</title>

    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <link href="../../plupload/js/jquery.plupload.queue/css/jquery.plupload.queue.css" rel="stylesheet" />
    <script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
    <script type="text/javascript" src="http://bp.yahooapis.com/2.4.21/browserplus-min.js"></script>

    <script src="../../plupload/js/plupload.full.js"></script>
    <script src="../../plupload/js/jquery.plupload.queue/jquery.plupload.queue.js"></script>
    <script src="../../plupload/js/i18n/fa.js"></script>

نکته : فایل fa.js  که جهت استفاده از زبان فارسی در اینترفیس آپلود فایل‌ها میباشد، که وجود آن در آدرس واضح میباشد.
سپس به فایل Views->Home->Index.cshtml بروید و آن را این گونه دوباره نویسی کنید :
 @{
    ViewBag.Title = "Uploading Files using PlUpload";
}
<h2>@ViewBag.Message</h2>

@using (Html.BeginForm("Post", "home", FormMethod.Post,
    new { enctype = "multipart/form-data" }))
{
    <div id="uploader">
        <p>You browser doesn't have Flash, Silverlight, Gears, BrowserPlus or HTML5 support.</p>
    </div>
}

<script>
    $(function () {

        $("#uploader").pluploadQueue({
            // General settings
            runtimes: 'html5,gears,flash,silverlight,browserplus,html4',
            url: '@Url.Action("Upload" , "Home")',
            max_file_size: '10mb',
            chunk_size: '1mb',
            unique_names: true,

            // Resize images on clientside if we can
            resize: { width: 320, height: 240, quality: 90 },

            // Specify what files to browse for
            filters: [
                { title: "Image files", extensions: "jpg,gif,png" },
                { title: "Zip files", extensions: "zip" }
            ],

            // Flash settings
            flash_swf_url: '/plupload/js/plupload.flash.swf',

            // Silverlight settings
            silverlight_xap_url: '/plupload/js/plupload.silverlight.xap'
        });
    });
</script>
توضیحات و نکات :
- جهت آپلود فایل‌ها تگ enctype = "multipart/form-data" را فراموش نکنید.
- در قسمت مقداردهی به ویژگی‌های Plupload  ، قسمت runtime  به صورت ترتیبی کار میکند لذا اگر اولی پشتیبانی نشود سراغ دومی میرود و اگر دومی نشود سومی و ... در صفحه‌ی اول سایت PlUpload ، موارد پشتیبانی شده توسط تکنولوژی‌ها آورده شده است لذا این ترتیب را ترتیب مناسبی میبینم و اگر اولین مورد html5 باشد امکان Drag&Drop وجود خواهد داشت.
خود سایت PlUpload  داکیومنت خیلی خوبی جهت توضیح موارد مختلف دارد لذا توضیح دوباره لازم نیست.
همان طور که در ویژگی url  مشاهده میکنید به کنترلر Home  و اکشن متود Upload اشاره شده است که طرز کار به این گونه است که هر بار که یک فایل آپلود میشود درخواستی به این آدرس و محتوای فایل در قسمت Request.Files ارسال میشود و همین طور نام فایل که unique ارسال میشود و chunk که تیکه‌های فایل است(پست میشود).
پس اکشنی با نام Upload  در کنترلر HomeController بسازید :
        [HttpPost]
        public ActionResult Upload(int? chunk, string name)
        {
            var fileUpload = Request.Files[0];
            var uploadPath = Server.MapPath("~/App_Data");
            chunk = chunk ?? 0;
            using (var fs = new FileStream(Path.Combine(uploadPath, name), chunk == 0 ? FileMode.Create : FileMode.Append))
            {
                if (fileUpload != null)
                {
                    var buffer = new byte[fileUpload.InputStream.Length];
                    fileUpload.InputStream.Read(buffer, 0, buffer.Length);
                    fs.Write(buffer, 0, buffer.Length);
                }
            }
            return Content("chunk uploaded", "text/plain");
        } 
توضیحات : ابتدا فایل مورد نظر از قسمت Request.Files واکشی میشود و سپس فایل را در پوشه App_Data ذخیره میکند. (یکی از چندین روش ذخیره سازی که مطالعه در این قسمت به خواننده واگذار میشود.)

حال برنامه را اجرا کنید و از این ابزار لذت ببرید:) 
نکته : قسمت فارسی ساز اونو تغییر دادم چون که ترجمه‌ی فارسی خودش یه سری نقایصی داشت که گویا از کار با google translate به وجود اومده بود!
مطالب
تبدیل پلاگین‌های jQuery‌ به کنترل‌های ASP.Net

امروز داشتم یک سری از پلاگین‌های jQuery را مرور می‌کردم، مورد زیر به نظرم واقعا حرفه‌ای اومد و کمبود آن هم در بین کنترل‌های استاندارد ASP.Net محسوس است:
Masked Input Plugin
استفاده از آن به صورت معمولی بسیار ساده است. فقط کافی است اسکریپت‌های jQuery و سپس این افزونه به هدر صفحه اضافه شوند و بعد هم مطابق صفحه usage آن عمل کرد.
خیلی هم عالی! ولی این شیوه‌ی متداول کار در ASP.Net نیست. آیا بهتر نیست این مجموعه را تبدیل به یک کنترل کنیم و از این پس به سادگی با استفاده از Toolbox ویژوال استودیو آن‌را به صفحات اضافه کرده و بدون درگیر شدن با دستکاری سورس html صفحه، از آن استفاده کنیم؟ به‌عبارتی دیگر یکبار باید با جزئیات درگیر شد، آنرا بسته بندی کرد و سپس بارها از آن استفاده نمود. (مفاهیم شیءگرایی)

برای این‌کار، یک پروژه جدید ایجاد ASP.Net server control را آغاز نمائید (به نام MaskedEditCtrl).



به صورت پیش فرض یک قالب استاندارد ایجاد خواهد شد که کمی نیاز به اصلاح دارد. نام کلاس را به MaskedEdit تغییر خواهیم داد و همچنین در قسمت ToolboxData نیز نام کنترل را به MaskedEdit ویرایش می‌کنیم.
برای اینکه مجبور نشویم یک کنترل کاملا جدید را از صفر ایجاد کنیم، خواص و توانایی‌های اصلی این کنترل را از TextBox استاندارد به ارث خواهیم برد. بنابراین تا اینجای کار داریم:
namespace MaskedEditCtrl
{
[DefaultProperty("MaskFormula")]
[ToolboxData("<{0}:MaskedEdit runat=server></{0}:MaskedEdit>")]
[Description("MaskedEdit Control")]
public class MaskedEdit : TextBox

{

سپس باید رویداد OnPreRender را تحریف (override) کرده و در آن همان اعمالی را که هنگام افزودن اسکریپت‌ها به صورت دستی انجام می‌دادیم با برنامه نویسی پیاده سازی کنیم. چند نکته ریز در اینجا وجود دارد که در ادامه به آن‌ها اشاره خواهد شد.
از ASP.Net 2.0 به بعد، امکان قرار دادن فایل‌های اسکریپت و یا تصاویر همراه یک کنترل، درون فایل dll آن بدون نیاز به توزیع مجزای آنها به صورت WebResource مهیا شده است. برای این منظور اسکریپت‌های jQuery و افزونه mask edit را به پروژه اضافه نمائید. سپس به قسمت خواص آنها (هر دو اسکریپت) مراجعه کرده و build action آنها را به Embedded Resource تغییر دهید (شکل زیر):



از این پس با کامپایل پروژه، این فایل‌ها به صورت resource به dll ما اضافه خواهند شد. برای تست این مورد می‌توان به برنامه reflector مراجعه کرد (تصویر زیر):



پس از افزودن مقدماتی اسکریپت‌ها و تعریف آنها به صورت resource ، باید آنها را در فایل AssemblyInfo.cs پروژه نیز تعریف کرد (به صورت زیر).

[assembly: WebResource("MaskedEditCtrl.jquery.min.js", "text/javascript")]
[assembly: WebResource("MaskedEditCtrl.jquery.maskedinput-1.1.4.pack.js", "text/javascript")]

نکته مهم: همانطور که ملاحظه می‌کنید نام فضای نام پروژه (namespace) باید به ابتدای اسکریپت‌های معرفی شده اضافه شود.

پس از آن نوبت به افزودن این اسکریپت‌ها به صورت خودکار در هنگام نمایش کنترل است. برای این منظور داریم:
        protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

//adding .js files
if (!Page.ClientScript.IsClientScriptIncludeRegistered("jquery_base"))
{
string scriptUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MaskedEditCtrl.jquery.min.js");
Page.ClientScript.RegisterClientScriptInclude("jquery_base", scriptUrl);
}

if (!Page.ClientScript.IsClientScriptIncludeRegistered("edit_ctrl"))
{
string scriptUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MaskedEditCtrl.jquery.maskedinput-1.1.4.pack.js");
Page.ClientScript.RegisterClientScriptInclude("edit_ctrl", scriptUrl);
}

if (!Page.ClientScript.IsStartupScriptRegistered("MaskStartup" + this.ID))
{
// Form the script to be registered at client side.
StringBuilder sbStartupScript = new StringBuilder();
sbStartupScript.AppendLine("jQuery(function($){");
sbStartupScript.AppendLine("$(\"#" + this.ClientID + "\").mask(\"" + MaskFormula + "\");");
sbStartupScript.AppendLine("});");
Page.ClientScript.RegisterStartupScript(typeof(Page),
"MaskStartup" + this.ID, sbStartupScript.ToString(), true);
}

}

همانطور که ملاحظه می‌کنید، ابتدا WebResource دریافت شده و سپس به صفحه اضافه می‌شود. در آخر مطابق راهنمای افزونه mask edit عمل شد. یعنی اسکریپت مورد نظر را ساخته و به صفحه اضافه کردیم.

نکته جاوا اسکریپتی: علت استفاده از this.ClientID جهت معرفی نام کنترل جاری این است که هنگامیکه کنترل توسط یک master page رندر شود، ID آن توسط موتور ASP.Net کمی تغییر خواهد کرد. برای مثال myTextBox‌ به ctl00_ContentPlaceHolder1_myTextBox تبدیل خواهد شد و اگر صرفا this.ID ذکر شده باشد دیگر دسترسی به آن توسط کدهای جاوا اسکریپت مقدور نخواهد بود. بنابراین از ClientID جهت دریافت ID نهایی رندر شده توسط ASP.Net کمک می‌گیریم.

در اینجا MaskFormula مقداری است که هنگام افزودن کنترل به صفحه می‌توان تعریف کرد.

[Description("MaskedEdit Formula such as 99/99/9999")]
[Bindable(true), Category("MaskedEdit"), DefaultValue(0)]
public string MaskFormula
{
get
{
if (ViewState["MaskFormula"] == null) ViewState["MaskFormula"] = "99/99/9999";
return (string)ViewState["MaskFormula"];
}
set { ViewState["MaskFormula"] = value; }

}

این خاصیت public هنگام نمایش در Visual studio به شکل زیر درخواهد آمد:



نکته مهم: در اینجا حتما باید از view state جهت نگهداری مقدار این خاصیت استفاده کرد تا در حین post back ها مقادیر انتساب داده شده حفظ شوند.

اکنون پروژه را کامپایل کنید. برای افزودن کنترل ایجاد شده به toolbox می‌توان مطابق تصویر عمل کرد:



نکته: برای افزودن آیکون به کنترل (جهت نمایش در نوار ابزار) باید: الف) تصویر مورد نظر از نوع bmp باشد با اندازه 16 در16 pixel . ب) باید آنرا به پروژه افزود و build action آن را به Embedded Resource تغییر داد. سپس آنرا در فایل AssemblyInfo.cs پروژه نیز تعریف کرد (به صورت زیر).

[assembly: System.Web.UI.WebResource("MaskedEditCtrl.MaskedEdit.bmp", "img/bmp")]

کنترل ما پس از افزوده شدن، شکل زیر را خواهد داشت:


جهت دریافت سورس کامل و فایل بایناری این کنترل، اینجا کلیک کنید.


مطالب
گروه بندی اطلاعات در jqGrid
فرض کنید لیستی از مطالب را به فرمت ذیل در اختیار داریم:
namespace jqGrid10.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string CategoryName { set; get; }
        public int NumberOfViews { set; get; }
    }
}
می‌خواهیم آن‌ها را با شرایط ذیل گروه بندی کنیم:
- گروه بندی بر روی ستون CategoryName انجام شود.
- ستونی که بر روی آن گروه بندی انجام می‌شود، نمایش داده نشود.
- در ابتدای نمایش گروه‌ها، تمام آن‌ها به صورت جمع شده و Collapsed نمایش داده شوند.
- پس از نمایش گروه‌ها، اولین گروه به صورت خودکار باز شود.
- تعداد ردیف هر گروه به عنوان گروه اضافه شود.
- جمع کل ستون تعداد بار مشاهدات هر گروه قابل محاسبه شود.
- جمع کل هر گروه در زمانیکه هر گروه نیز بسته‌است نمایش داده شود.
- رنگ ردیف جمع کل قابل تنظیم باشد.



فعال سازی گروه بندی در jqGrid

فعال سازی گروه بندی در jqGrid به سادگی افزودن تعاریف ذیل است:
            $('#list').jqGrid({
                caption: "آزمایش دهم",
                //...
                grouping: true,
                groupingView: {
                    groupField: ['CategoryName'],
                    groupOrder: ['asc'],
                    groupText : ['<b>{0} - {1} ردیف</b>'],
                    groupDataSorted: true,
                    groupColumnShow: false,
                    groupCollapse: true,
                    groupSummary: [true],
                    showSummaryOnHide: true
                }
            });
grouping: true سبب می‌شود تا گروه بندی فعال شود. سپس نیاز است ستون یا ستون‌هایی را که قرار است بر روی آن‌ها گروه بندی صورت گیرد، معرفی کنیم. اینکار توسط خواص شیء groupingView انجام می‌شوند.
groupField بیانگر آرایه‌ای از ستون‌هایی است که قرار است بر روی آن‌ها گروه بندی صورت گیرد.
groupOrder آرایه‌ای اختیاری از مقادیر asc یا desc است که متناظر هستند با نحوه‌ی مرتب سازی پیش فرض ستون‌های معرفی شده در آرایه groupField.
groupText آرایه‌ای اختیاری از عناوین گروه بندی‌های انجام شده‌است. اگر ذکر شود، {0} آن با نام گروه و {1} آن با تعداد عناصر گروه جایگزین می‌شود.
تنظیم groupDataSorted سبب خواهد شد تا نام ستونی که بر روی آن گروه بندی صورت می‌گیرد، به سرور ارسال شود (توسط پارامتر sidx). به این ترتیب در سمت سرور می‌توان اطلاعات را به صورت پویا مرتب سازی کرده و بازگشت داد.
با تنظیم groupColumnShow به false سبب خواهیم شد تا ستون‌های معرفی شده در قسمت groupField نمایش داده نشوند.
با تنظیم groupCollapse به true، در ابتدای نمایش گروه‌ها، ردیف‌های آن‌ها نمایش داده نخواهند شد و در حالت جمع شده قرار می‌گیرند.
groupSummary به معنای فعال سازی نمایش ردیف محاسبه‌ی summary مانند sum، min، max و امثال آن بر روی یک گروه است.
اگر مقدار showSummaryOnHide مساوی true باشد، ردیف محاسبه‌ی summary حتی در حالت groupCollapse: true نمایش داده خواهد شد.


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

برای فعال سازی نهایی محاسبه‌ی جمع ستون تعداد بار مشاهدات، علاوه بر تنظیم groupSummary به true، نیاز است در همان ستون مشخص کنیم که این محاسبات چگونه باید انجام شوند:
                colModel: [
// ........
                    {
                        name: '@(StronglyTyped.PropertyName<Post>(x => x.Title))',
                        index: '@(StronglyTyped.PropertyName<Post>(x => x.Title))',
                        align: 'right',
                        width: 150,
                        summaryTpl: '<div style="text-align: left;">خلاصه </div>',
                        summaryType: function (val, name, record) {
                            return "";
                        }
                    },
// ........
                    {
                        name: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))',
                        index: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))',
                        align: 'center',
                        width: 70,
                        summaryType: 'sum', summaryTpl: '<b>جمع مشاهدات: {0}</b>'
                    }
                ],
خاصیت summaryType نوع متد محاسبه‌ی summary مانند sum را مشخص می‌کند. خاصیت summaryTpl اختیاری است. اگر ذکر شود سبب فرمت مقدار محاسبه شده‌ی نهایی می‌گردد. در اینجا {0} به مقدار نهایی که قرار است در این سلول درج شود، اشاره می‌کند.
summaryType مانند ستون عنوان، سفارشی شده نیز می‌توان باشد. در ردیف summary و ستون عنوان تنها می‌خواهیم یک مقدار ثابت را نمایش دهیم، به همین جهت summaryType آن به یک مقدار خالی تنظیم شده‌است.


تغییر رنگ ردیف خلاصه عملیات هر گروه به همراه گشودن خودکار اولین گروه

گروه بندی به همراه یک سری متد توکار نیز هست. برای مثال اگر متد groupingToggle را بر روی Id هر گروه فراخوانی کنیم، می‌توان سبب باز یا بسته شده آن گروه شد. متدهای دیگری مانند groupingGroupBy برای گروه بندی پویا و groupingRemove برای حذف گروه بندی نیز وجود دارند:
            $('#list').jqGrid({
                caption: "آزمایش دهم",
                //.........
                loadComplete: function() {
                    //.........
                    $('#list').jqGrid('groupingToggle', 'list' + 'ghead_0_0');
                    $("tr.jqfoot td").css({
                        "background": "#2f4f4f",
                        "color": "#FFF"
                    });
                },
            });
بهترین محل اعمال رنگ به ردیف‌های summary، در روال رویدادگردان loadComplete گرید است.


مثال کامل این قسمت را از اینجا می‌توانید دریافت کنید:
jqGrid10.zip
مطالب
دستکاری کردن عملیات Sort در SQL Server
گاهی اوقات لازم می‌باشد، در زمان Sort نمودن یک ستون، تمایل داشته باشیم Range خاصی از مقادیر آن ستون در ابتدا قرار گیرد، و عملیات Sort پس از آن Range، اعمال گردد. برای انجام چنین کاری می‌توانیداز روش زیر استفاده نمایید:
برای درک مطلب مثالی می‌زنیم:
در ابتدا Script زیر را اجرا نمایید، که شامل یک جدول و درج چند رکورد در آن می‌باشد:
Create Table TestSort
(ID  int identity(1,1),
Name nvarchar(30),
Color nvarchar(15)
)
درج رکورد:
Insert into TestSort (Name,color)
Values  ('Adjustable Race',null)
       ,('Bearing Ball',null)
       ,('Headset Ball Bearings',null)
       ,('LL Crankarm','Black')
       ,('ML Crankarm','Black')
       ,('Chainring','Black')
       ,('Front Derailleur Cage','Silver')
       ,('Front Derailleur Linkage','Silver')
       ,('Lock Ring','Silver')
       ,('HL Road Frame - Red, 62','Red')
       ,('HL Road Frame - Red, 48','Red')
       ,('LL Road Frame - Red, 44','Red')
       ,('Full-Finger Gloves, M','RED')
       ,('Road-550-W Yellow, 38','Yellow')
       ,('Road-550-W Yellow, 40','Yellow')
       ,('Road-550-W Yellow, 42','Yellow')
       ,('Classic Vest, S','Blue')
       ,('Classic Vest, M','Blue')
       ,('Classic Vest, L','Blue')
در جدول TestSort ستونی به نام Color داریم، که نام رنگها در آن درج شده است، رنگهایی همچون  Black ، Silver،Blue،Yellow و Red
درابتدا روی ستون Color بصورت نرمال Sort صعودی انجام می‌دهیم:
Select t.ID,t.Name,t.Color from TestSort as t order by t.Color
خروجی:

مطابق شکل،زمانی که Sort بصورت صعودی است، رکوردهایی را که ستون Color آنها دارای مقدار Null می‌باشند، در ابتدای جدول قرار گرفته اند.
در ادامه می‌خواهیم، عملیات Sort ی را روی ستون Color انجام دهیم، بطوریکه تمامی رکوردهایی که مقدار ستون Color شان Red است، در ابتدای جدول قرار گیرد، و پس از آن عملیات سورت روی رنگهای دیگر اعمال شود.
برای انجام چنین کاری کافیست Script زیر را اجرا نمایید:
Select t.ID,t.Name,t.Color from TestSort as t 
Where t.color is not null
order by  Case t.Color
When 'Red' Then Null
Else t.color 
End;
خروجی:

چطور اینکار انجام شد:
اگر بهScript ذکر شده دقت نمایید، در قسمت Order by اشاره کردیم، تمام مقادیر Red در ستون Color به Null تغییر کنند، بنابراین SQL Server، در ابتدا مقادیر Red را یافته آنها را به Null تغییر و سپس عملیات سورت را انجام می‌دهد،
برای درک بیشتر عملیاتی را که SQL Server پشت صحنه انجام می‌دهد با Script  زیر قابل شبیه سازی میباشد:
Select * into Simulation from 
(Select t.ID,t.Name,t.Color,
Case t.Color
When 'Red' Then Null
Else t.color 
End RedNull
from TestSort as t 
Where t.color is not null) A
سپس روی ستون RedNull از جدول Simulation سورت انجام می‌دهیم:
Select * from Simulation order by Rednull
خروجی:

مطابق شکل،پشت صحنه SQL Server چنین کاری را انجام می‌دهد، و در زمان نمایش ستون RedNull پنهان یا حذف می‌گردد، و ستون Color، Name و ID نمایش داده می‌شود.
امیدوارم مفید واقع شده باشد.