نظرات مطالب
امکان تغییر شکل سراسری URLهای تولیدی توسط برنامه‌های ASP.NET Core 2.2
به نظر شما استفاده از یک TagHelper  سفارشی راه درستی هست برای حل این مورد
public class AnchorTagHelper : TagHelper {
        /// <summary>
        /// The name of the action method.
        /// </summary>
        [HtmlAttributeName ("asp-action")]
        public string Action { get; set; }

        /// <summary>
        /// The name of the controller.
        /// </summary>
        [HtmlAttributeName ("asp-controller")]
        public string Controller { get; set; }

        /// <summary>
        /// The name of the area.
        /// </summary>
        [HtmlAttributeName ("asp-area")]
        public string Area { get; set; }

        [HtmlAttributeName ("asp-route")]
        public string Route { get; set; }

        // Can be async Task
        public override void Process (TagHelperContext context, TagHelperOutput output) {
            output.TagName = "a";

            string result = string.Empty;
            if (!string.IsNullOrWhiteSpace (Area)) {
                result += "/" + Area;
            }

            if (!string.IsNullOrWhiteSpace (Controller)) {
                result += "/" + Controller;
            }

            if (!string.IsNullOrWhiteSpace (Action)) {
                result += "/" + Action;
            }
            if (!string.IsNullOrWhiteSpace (Route)) {
                Route = ToFriendlyHref (Route);
                result += "/" + Route;
            }

            output.Attributes.SetAttribute ("href", result.ToLowerInvariant ());
            //output.Content.SetContent (currentAttribute.ToString ());
        }

        private string ToFriendlyHref (object value) {
            string text = value.ToString ();
            List<char> illegalChars = new List<char> () { ' ', '.', '#', '%', '&', '*', '{', '}', '\\', ':', '<', '>', '?', ';', '@', '=', '+', '$', ',' };
            illegalChars.ForEach (c => {
                text = text.Replace (c.ToString (), "-");
            });
            return text;
        }
    }

نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرت‌ها به یک اسمبلی دیگر
ایجاد نام جداول به صورت جمع (Pluralized) و داینامیک :
اگه از روش خودکار کردن تعاریف DbSet ها استفاده کرده باشیم چون دیگر در داخل کلاس Context برنامه، Dbset تعریف نمی‌شود معمولا برای نام گذاری جداول که به صورت جمع باشند از اتریبیوت Table استفاده می‌کنیم.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

[Table("Users")]
public class User : BaseEntity
{
    public long Id { get; set; }

    [Required]
    public string FullName { get; set; } = null!;
}
برای اینکه نام جداول به صورت خودکار به صورت جمع ایجاد شوند میتوان از این متد استفاده کرد:
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;

namespace ProjectName.Common.EfHelpers;

public static class EfToolkit
{
    /// <summary>
    /// ایجاد نام موجودیت‌ها
    /// نام موجودیت اگر اتریبیوت تیبل داشته باشد
    /// از همان نام استفاده میشود و اگر نداشته باشد
    /// نامش جمع بسته میشود
    /// </summary>
    /// <param name="builder"></param>
    public static void MakeTableNamesPluralized(this ModelBuilder builder)
    {
        var entityTypes = builder.Model.GetEntityTypes();

        foreach (var entityType in entityTypes)
        {
            // Get the CLR type of the entity
            var entityClrType = entityType.ClrType;
            var hasTableAttribute = entityClrType.GetCustomAttribute<TableAttribute>();

            // Apply the pluralized table name for the entity
            if (hasTableAttribute is null)
            {
                // Get the pluralized table name
                var pluralizedTableName = GetPluralizedTableName(entityClrType);
                builder.Entity(entityClrType).ToTable(pluralizedTableName);
            }
        }
    }

    /// <summary>
    /// گرفتن نام تایپ و عوض کردن نام آن از مفرد به جمع
    /// Singular to plural
    /// Category => Categories
    /// Box => Boxes
    /// Bus => Buses
    /// Computer => Computers
    /// </summary>
    /// <param name="entityClrType"></param>
    /// <returns></returns>
    private static string GetPluralizedTableName(Type entityClrType)
    {
        // Example implementation (Note: This is a simple pluralization logic and might not cover all cases)
        var typeName = entityClrType.Name;
        if (typeName.EndsWith("y"))
        {
            // Substring(0, typeName.Length - 1)
            // Range indexer
            var typeNameWithoutY = typeName[..^1];
            return typeNameWithoutY + "ies";
        }

        if (typeName.EndsWith("s") || typeName.EndsWith("x"))
        {
            return typeName + "es";
        }
        return typeName + "s";
    }
}
نحوه فراخوانی در متد OnModelCreating:
protected override void OnModelCreating(ModelBuilder builder)
{
    // it should be placed here, otherwise it will rewrite the following settings!
    base.OnModelCreating(builder);
    builder.RegisterAllEntities(typeof(BaseEntity));
    builder.MakeTableNamesPluralized();
    builder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
هر موجودیتی که اتریبیوت Table داشته باشد از همان نام برای جدول استفاده می‌شود در غیر اینصورت نام انتیتی به صورت جمع ایجاد می‌گردد.
قبل از اینکه نام جداول نیز جمع بسته شود تمامی موجودیت‌ها به صورت خودکار اضافه می‌شوند.
public static class EfToolkit
{
    /// <summary>
    /// ثبت تمامی انتیتی‌ها
    /// <param name="builder"></param>
    /// <param name="type"></param>
    /// </summary>
    public static void RegisterAllEntities(this ModelBuilder builder, Type type)
    {
        var entities = type.Assembly.GetTypes()
            .Where(x => x.BaseType == type);
        foreach (var entity in entities)
            builder.Entity(entity);
    }
}  
هر کلاسی در لایه موجودیت‌ها از BaseEntity ارث بری کرده باشد به عنوان یک Entity شناخته می‌شود.
مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 6#
در ادامه پست پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 5# ، در این پست به تشریح کلاس دایره و بیضی می‌پردازیم.

ابتدا به تشریح کلاس ترسیم بیضی (Ellipse) می‌پردازیم.
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Ellipse Draw
    /// </summary>
    public class Ellipse : Shape
    {
        #region Constructors (2)

        /// <summary>
        /// Initializes a new instance of the <see cref="Ellipse" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Ellipse(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
            : base(startPoint, endPoint, zIndex, foreColor, thickness, isFill, backgroundColor)
        {
            ShapeType = ShapeType.Ellipse;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Ellipse" /> class.
        /// </summary>
        public Ellipse()
        {
            ShapeType = ShapeType.Ellipse;
        }

        #endregion Constructors

        #region Methods (1)

        // Public Methods (1) 

        /// <summary>
        /// Draws the specified g.
        /// </summary>
        /// <param name="g">The g.</param>
        public override void Draw(Graphics g)
        {
            if (IsFill)
                g.FillEllipse(BackgroundBrush, StartPoint.X, StartPoint.Y, Width, Height);
            g.DrawEllipse(Pen, StartPoint.X, StartPoint.Y, Width, Height);
            base.Draw(g);
        }

        #endregion Methods
    }
}
این کلاس از شی Shape ارث برده و دارای دو سازنده ساده می‌باشد که نوع شی ترسیمی را مشخص می‌کنند، در متد Draw نیز با توجه به توپر یا توخالی بودن شی ترسیم آن انجام میشود، در این کلاس باید متد HasPointInShape بازنویسی (override) شود، در این متد باید تعیین شود که یک نقطه در داخل بیضی قرار گرفته است یا خیر که متاسفانه فرمول بیضی خاطرم نبود. البته به صورت پیش فرض نقطه با توجه به چهارگوشی که بیضی را احاطه می‌کند سنجیده می‌شود.

کلاس دایره (Circle) از کلاس بالا (Ellipse) ارث بری دارد که کد آن را در زیر مشاهده می‌نمایید.
using System;
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Circle
    /// </summary>
    public class Circle : Ellipse
    {
#region Constructors (2) 

        /// <summary>
        /// Initializes a new instance of the <see cref="Circle" /> class.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        /// <param name="zIndex">Index of the z.</param>
        /// <param name="foreColor">Color of the fore.</param>
        /// <param name="thickness">The thickness.</param>
        /// <param name="isFill">if set to <c>true</c> [is fill].</param>
        /// <param name="backgroundColor">Color of the background.</param>
        public Circle(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
        {
            float x = 0, y = 0;
            float width = Math.Abs(endPoint.X - startPoint.X);
            float height = Math.Abs(endPoint.Y - startPoint.Y);
            if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = startPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = endPoint.X;
                y = endPoint.Y;
            }
            else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y)
            {
                x = endPoint.X;
                y = startPoint.Y;
            }
            else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y)
            {
                x = startPoint.X;
                y = endPoint.Y;
            }
            StartPoint = new PointF(x, y);
            var side = Math.Max(width, height);
            EndPoint = new PointF(x + side, y + side);
            ShapeType = ShapeType.Circle;
            Zindex = zIndex;
            ForeColor = foreColor;
            Thickness = thickness;
            BackgroundColor = backgroundColor;
            IsFill = isFill;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Circle" /> class.
        /// </summary>
        public Circle()
        {
            ShapeType = ShapeType.Circle;
        }

#endregion Constructors 

#region Methods (1) 

// Public Methods (1) 

        /// <summary>
        /// Points the in sahpe.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <param name="tolerance">The tolerance.</param>
        /// <returns>
        ///   <c>true</c> if [has point in sahpe] [the specified point]; otherwise, <c>false</c>.
        /// </returns>
        public override bool HasPointInSahpe(PointF point, byte tolerance = 5)
        {
            float width = Math.Abs(EndPoint.X+tolerance - StartPoint.X-tolerance);
            float height = Math.Abs(EndPoint.Y+tolerance - StartPoint.Y-tolerance);
            float diagonal = Math.Max(height, width);
            float raduis = diagonal / 2;
            float dx = Math.Abs(point.X - (X + Width / 2));
            float dy = Math.Abs(point.Y - (Y + height / 2));
            return (dx + dy <= raduis);
        }

#endregion Methods 
    }
}
این کلاس شامل دو سازنده می‌باشد، که در سازنده اول با توجه به نقاط ایتدا و انتهای ترسیم شکل مقدار طول و عرض مستطیل احاطه کننده دایره محاسبه شده و باتوجه به آنها بزرگترین ضلع به عنوان قطر دایره در نظر گرفته می‌شود و EndPoint شکل مورد نظر تعیین می‌شود.

در متد HasPointInShape  با استفاده از فرمول دایره تعیین می‌شود که آیا نقطه پارامتر ورودی متد در داخل دایره واقع شده است یا خیر (جهت انتخاب شکل برای جابجایی یا تغییر اندازه).
در پست‌های بعد به پیاده سازی اینترفیس نرم افزار خواهیم پرداخت.

موفق و موید باشید

در ادامه مطالب قبل:
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 4# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 5# 
مطالب
توسعه سیستم مدیریت محتوای 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 زیر استفاده کنید.

مطالب
آشنایی با قابلیت FileStream اس کیوال سرور 2008 - قسمت سوم

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

alter table tbl_files set(filestream_on ='default')

go
alter table tbl_files
add

[systemfile] varbinary(max) filestream null ,
FileId uniqueidentifier not null rowguidcol unique default (newid())
go

در ادامه جدول tblFiles قسمت قبل را در نظر بگیرید:

CREATE TABLE [tblFiles](
[FileId] [uniqueidentifier] ROWGUIDCOL NOT NULL,
[Title] [nvarchar](255) NOT NULL,
[SystemFile] [varbinary](max) FILESTREAM NULL,
UNIQUE NONCLUSTERED
(
[FileId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] FILESTREAM_ON [fsg1]

ALTER TABLE [dbo].[tblFiles] ADD DEFAULT (newid()) FOR [FileId]
GO

نحوه‌ی افزودن رکوردی جدید به جدول tblFiles :

INSERT INTO [tblFiles]
(
[Title],
[SystemFile]
)
VALUES
(
'file-1',
CAST('data data data' AS VARBINARY(MAX))
)
در اینجا سعی کرده‌ایم یک رشته ساده را در فیلدی از نوع فایل استریم ذخیره کنیم که روش کار به صورت فوق است. از آنجائیکه مقدار پیش فرض FileId را هنگام تعریف جدول به NEWID تنظیم کرده‌ایم، نیازی به ذکر آن نیست و به صورت خودکار محاسبه و ذخیره خواهد شد.
اگر کنجکاو باشید که این فایل اکنون کجا ذخیره شده و نحوه‌ی مدیریت آن توسط اس کیوال سرور به چه صورتی است، فقط کافی است به مسیری که هنگام افزودن گروه فایل‌ها و فایل مربوطه در تنظیمات خواص دیتابیس در قسمت قبل مشخص کردیم، مراجعه کرد (شکل زیر).



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

using System;
using System.IO;
using System.Data.SqlClient;
using System.Data;

namespace FileStreamTest
{
class CFS
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);

//خواندن اطلاعات فایل در آرایه‌ای از بایت‌ها
byte[] buffer = File.ReadAllBytes(filePath);

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//ساخت عبارت افزودن پارامتری
using (SqlCommand objSqlCmd = new SqlCommand(
"INSERT INTO [tblFiles]([Title],[SystemFile]) VALUES(@title , @file)",
objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.Text;

//تعریف وضعیت پارامترها و مقدار دهی آن‌ها
objSqlCmd.Parameters.AddWithValue("@title", title);
objSqlCmd.Parameters.AddWithValue("@file", buffer);

//اجرای فرامین
objSqlCmd.ExecuteNonQuery();
}

//پایان تراکنش
objSqlTran.Commit();
}
}
}

/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایه‌ای از بایت‌ها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] data = null;

using (SqlConnection objConn = new SqlConnection())
{
//کوئری اس کیوال پارامتری جهت دریافت محتویات فایل
string cmdText = "SELECT SystemFile FROM tblFiles WHERE FileId=@id";
using (SqlCommand objCmd = new SqlCommand(cmdText, objConn))
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objConn.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objConn.Open();

//تنظیم کردن وضعیت و مقدار پارامتر تعریف شده در کوئری
objCmd.Parameters.AddWithValue("@id", fileId);

//اجرای فرامین و دریافت فایل
using (SqlDataReader objread = objCmd.ExecuteReader())
{
if (objread != null)
if (objread.Read())
{
if (objread["SystemFile"] != DBNull.Value)
data = (byte[])objread["SystemFile"];
}
}
}
}

return data;
}
}
}

مثالی در مورد روش استفاده از کلاس فوق :

using System.IO;

namespace FileStreamTest
{
class Program
{
static void Main(string[] args)
{
CFS.AddNewRecord(@"C:\filest05.PNG", "test1");

//آی دی رکورد ذخیره شده در دیتابیس برای مثال
byte[] data = CFS.GetDataFromDb("BB848D45-382C-4D95-BF4E-52C3509407D4");
if (data != null)
{
File.WriteAllBytes(@"C:\tst.PNG", data);
}
}
}
}
روش فوق با روش متداول افزودن یک فایل به دیتابیس اس کیوال سرور هیچ تفاوتی ندارد و این‌جا هم بدون مشکل کار می‌کند. اطلاعات نهایی به صورت فایل‌هایی بر روی سیستم که توسط اس کیوال سرور مدیریت خواهند شد و با جدول شما یکپارچه‌اند، ذخیره می‌شوند.

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

CREATE PROCEDURE [AddFile](@Title NVARCHAR(255), @filepath VARCHAR(MAX) OUTPUT)
AS
BEGIN
SET NOCOUNT ON;

DECLARE @ID UNIQUEIDENTIFIER
SET @ID = NEWID()

INSERT INTO [tblFiles]
(
[FileId],
[title],
[SystemFile]
)
VALUES
(
@ID,
@Title,
CAST('' AS VARBINARY(MAX))
)

SELECT @filepath = SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
GO

CREATE PROCEDURE [GetFilePath](@Id VARCHAR(50))
AS
BEGIN
SET NOCOUNT ON;

SELECT SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
در رویه ذخیره شده AddFile ، ابتدا رکوردی بر اساس عنوان دلخواه ورودی با یک فایل خالی ایجاد می‌شود. سپس مسیر سیستمی این فایل را در آرگومان خروجی filepath قرار می‌دهیم. SystemFile.PathName از اس کیوال سرور 2008 جهت فیلدهای فایل استریم به اس کیوال سرور اضافه شده است. از این مسیر در برنامه خود جهت نوشتن بایت‌های فایل مورد نظر در آن توسط شیء SqlFileStream استفاده خواهیم کرد.
رویه ذخیره شده GetFilePath نیز تنها مسیر سیستمی فایل استریم ذخیره شده را بر می‌گرداند.
به این ترتیب کدهای برنامه به صورت زیر تغییر خواهند کرد:

using System.Data.SqlClient;
using System.Data;
using System.Data.SqlTypes;
using System.IO;

namespace FileStreamTest
{
class CFSqlFileStream
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);

//خواندن اطلاعات فایل در آرایه‌ای از بایت‌ها
byte[] buffer = File.ReadAllBytes(filePath);

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده افزودن فایل
using (SqlCommand objSqlCmd = new SqlCommand(
"AddFile", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;

//مشخص ساختن وضعیت و مقدار پارامتر عنوان
SqlParameter objSqlParam1 = new SqlParameter("@Title", SqlDbType.NVarChar, 255);
objSqlParam1.Value = title;

//مشخص ساختن پارامتر خروجی رویه ذخیره شده
SqlParameter objSqlParamOutput = new SqlParameter("@filepath", SqlDbType.VarChar, -1);
objSqlParamOutput.Direction = ParameterDirection.Output;

//افزودن پارامترها به شیء کامند
objSqlCmd.Parameters.Add(objSqlParam1);
objSqlCmd.Parameters.Add(objSqlParamOutput);

//اجرای رویه ذخیره شده
objSqlCmd.ExecuteNonQuery();

//و سپس دریافت خروجی آن
string Path = objSqlCmd.Parameters["@filepath"].Value.ToString();

//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای نوشتن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازه‌های اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();
using (SqlFileStream objSqlFileStream =
new SqlFileStream(Path, objContext, FileAccess.Write))
{
objSqlFileStream.Write(buffer, 0, buffer.Length);
}
}
}

objSqlTran.Commit();
}
}
}

/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایه‌ای از بایت‌ها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] buffer = null;

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده دریافت مسیر فایل
using (SqlCommand objSqlCmd =
new SqlCommand("GetFilePath", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;

//مشخص ساختن پارامتر ورودی رویه ذخیره شده و مقدار دهی آن
SqlParameter objSqlParam1 = new SqlParameter("@ID", SqlDbType.VarChar, 50);
objSqlParam1.Value = fileId;
objSqlCmd.Parameters.Add(objSqlParam1);

//اجرای رویه ذخیره شده و دریافت مسیر سیستمی فایل استریم
string path = string.Empty;
using (SqlDataReader sdr = objSqlCmd.ExecuteReader())
{
sdr.Read();
path = sdr[0].ToString();
}

//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای خواندن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازه‌های اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();

using (SqlFileStream objSqlFileStream =
new SqlFileStream(path, objContext, FileAccess.Read))
{
buffer = new byte[(int)objSqlFileStream.Length];
objSqlFileStream.Read(buffer, 0, buffer.Length);
}
}
}

objSqlTran.Commit();
}
}

return buffer;
}
}
}
در پایان برای تکمیل بحث می‌توان به مقاله‌ی مرجع زیر مراجعه کرد:
FILESTREAM Storage in SQL Server 2008

مطالب
نمایش یک پیغام به کاربر در ASP.Net

عموما در برنامه‌های وب مرسوم است که پیغام به کاربر را در همان لابلای html صفحه نمایش می‌دهند. مثلا یک برچسب و سپس تنظیم متن آن در کد برنامه به صورت پویا.
با استفاده از پلاگین‌های jQuery این‌کار را به صورت شکیل‌تری می‌توان انجام داد. برای مثال:


پلاگین کم حجمی برای این منظور موجود است به نام jQuery Notice (یکی از چند ده نمونه موجود)
<script type="text/javascript">
$(document).ready(function()
{
jQuery.noticeAdd({
text: 'پیغامی به کاربر',
stay: false
});
});
</script>
کمی این پلاگین را اصلاح کردم تا مشکل عدم نمایش آن هنگام اسکرول طولانی صفحه در IE حل شود (به صورت پیش فرض با فایرفاکس مشکلی ندارد). برای مثال این div را در نظر بگیرید:
<div id="myElement" style="position: absolute">This stays at the top</div>
قصد داریم مکان آن‌را در بالای صفحه ثابت کنیم (حتی با یک اسکرول طولانی مانند تصویر فوق، باز هم همان بالا باقی بماند و قابل مشاهده باشد).
با استفاده از jQuery این‌کار به صورت زیر قابل انجام است:
<script type="text/javascript">
$(document).ready(function()
{
$(window).scroll(function() {
$('#myElement').css('top', $(this).scrollTop() + "px");
});
});
</script>
زمانیکه scroll ایی در window جاری صورت ‌گیرد، div ایی با id مساوی myElement یافت شده و سپس مقدار top آن تنظیم شده و در بالای صفحه نمایش داده می‌شود.

ولی این روش جهت نمایش پیغامی پویا به کاربر مشکل دارد.
نیاز است به ازای هر پیغام پویا یکبار به نحوی این اسکریپت به صفحه تزریق شود که روش انجام کار در ASP.Net به صورت زیر می‌تواند باشد:

using System;
using System.Web.UI;
using System.Web;

public class CAddJqueryNotice
{
/// <summary>
/// نمایش یک پیغام بر اساس پلاگین نوتیس
/// </summary>
/// <param name="title">عنوان</param>
/// <param name="msg">پیغام</param>
/// <param name="rtl">راست به چپ؟</param>
/// <param name="duration">مدت زمان نمایش</param>
/// <param name="autoHide">به صورت خودکار بسته شود؟</param>
public static void Show(string title, string msg, bool rtl, int duration, bool autoHide)
{
string scriptBlock
= string.Format(@"<script type=""text/javascript"">
$(document).ready(function() {{
jQuery.noticeAdd({{
text: '<b>{0}</b><br/><div align=left dir={1}>{2}</div>',
stay: {3},
stayTime: {4}
}});
}});
</script>",
title,
(rtl ? "rtl" : "ltr"),
msg,
(autoHide ? "false" : "true"),
duration);

if (HttpContext.Current == null || HttpContext.Current.Handler == null) return;
Page page = HttpContext.Current.Handler as Page;
if (page != null)
page.ClientScript.RegisterStartupScript(
page.GetType(),
"script" + new Guid().ToString("N"),
scriptBlock,
false);
}

}
از آنجائیکه در یک کلاس دیگر خارج از صفحه اصلی مشغول به کار هستیم، دسترسی مستقیم به شیء Page و سپس متد ClientScript.RegisterStartupScript آن جهت تزریق اسکریپت خود به صفحه نداریم. اما با استفاده از HttpContext.Current.Handler می‌توان به این مقصود رسید و مشکل حل می‌شود.

برای آزمایش آن یک دکمه را در صفحه قرار داده و در روال رخ‌داد گردان کلیک آن کد زیر را اضافه کنید:
CAddJqueryNotice.Show( "لطفا دوباره سعی کنید", "مشکلی رخ داده است", true, 2000, true);

بدیهی است قبل از استفاده از کد فوق، باید چند سطر زیر را به هدر master page سایت خود اضافه کنید:
<script src="jquery-1.3.2.js" type="text/javascript"></script>
<link href="jquery.notice.css" type="text/css" media="screen" rel="stylesheet" />
<script src="jquery.notice.js" type="text/javascript"></script>

مطالب
String.format در جاوا اسکریپت
مقدمه 
با اینکه زبان برنامه نویسی جاوا اسکریپت زبانی بسیار قدرتمند و با امکانات زیاد است، اما فقدان برخی متدهای کمکی پرمصرف در آن در برخی موارد باعث دردسرهایی می‌شود. امکانی برای فرمت‌بندی رشته‌ها یکی از این نیازهای نسبتا پرکاربرد است.
متدی که در این مطلب قصد توضیح پیاده‌سازی آنرا داریم، String.format نام دارد که فرایندی مشابه متد متناظر در دات نت را انجام می‌دهد. هم‌چنین سعی شده است تا نحوه پیاده‌سازی این متد کمکی از ابتدایی‌ترین نمونه‌ها تا نسخه‌های پیشرفته‌تر برای درک بهتر مطلب نشان داده شود.
.
پیاده‌سازی متد String.format
1. در این پیاده‌سازی از اولین فرایندی که ممکن است به ذهن یک برنامه‌نویس خطور کند استفاده شده است. این پیاده‌سازی بسیار ساده به صورت زیر است:
String.format = function () {
  var s = arguments[0];
  for (var i = 0; i < arguments.length - 1; i++) {
    s = s.replace("{" + i + "}", arguments[i + 1]);
  }
  return s;
};

2. پیاده‌سازی مشابهی هم با استفاده از نوع دیگری از حلقه for که تقریبا! مشابه با حلقه foreach در #C است به صورت زیر می‌توان درنظر گرفت:
String.format = function () {
  var s = arguments[0];
  for (var arg in arguments) {
    var i = parseInt(arg);
    s = s.replace("{" + i + "}", arguments[i + 1]);
  }
  return s;
};
در این متدها ابتدا فرمت واردشده توسط کاربر از لیست آرگومان‌های متد خوانده شده و در متغیر s ذخیره می‌شود. سپس درون یک حلقه به ازای هر توکن موجود در رشته فرمت، یک عملیات replace با مقدار متناظر در لیست آرگومان‌های متد انجام می‌شود. نحوه استفاده از این متد نیز به صورت زیر است:
console.log(String.format("{0} is nice!", "donettips.info"));
هر دو متد خروجی یکسانی دارند، به صورت زیر:
donettips.info is nice!
تا اینجا به نظر می‌رسد که عملیات به‌درستی پیش می‌رود. اما اولین و بزرگ‌ترین مشکل در این دو متد نحوه کارکردن متد replace در جاوا اسکریپت است. این متد با این نحوه فراخوانی تنها اولین توکن موجود را یافته و عملیات جایگزینی را برای آن انجام می‌دهد. برای روشن‌تر شدن موضوع به مثال زیر توجه کنید:
console.log(String.format("{0} is {1} nice! {0} is {1} nice!", "donettips.info", "very"));
با اجرای این مثال نتیجه زیر حاصل می‌شود:
donettips.info is very nice! {0} is {1} nice!
همان‌طور که می‌بنید عملیات replace برای سایر توکن‌ها انجام نمی‌شود.

3. برای حل مشکل فوق می‌توان از روش ساده زیر استفاده کرد:
String.format = function () {
  var original = arguments[0],
      replaced;
  for (var i = 0; i < arguments.length - 1; i++) {
    replaced = '';
    while (replaced != original) {
      original = replaced || original;
      replaced = original.replace("{" + i + "}", arguments[i + 1]);
    }
  }
  return replaced;
};
در این روش عملیات replace تا زمانی‌که تغییری در رشته جاری ایجاد نشود ادامه می‌یابد. با استفاده از این متد، خروجی مثال قبل درست و به صورت زیر خواهد بود:
donettips.info is very nice! donettips.info is very nice!

4. راه حل دیگر استفاده از امکانات شی RegExp در دستور replace است. نکته مهم استفاده از modifier کلی یا global (که با حرف g مشخص می‌شود) در شی تولیدی از RegExp است (^ و ^ و ^، برای جلوگیری از دورشدن از بحث اصلی، جستجو برای کسب اطلاعات بیشتر در این زمینه به خوانندگان واگذار می‌شود). برای استفاده از این شی متد ما به صورت زیر تغییر می‌کند:
String.format = function () {
  var s = arguments[0];
  for (var i = 0; i < arguments.length - 1; i++) {
    s = s.replace(new RegExp("\\{" + i + "\\}", "g"), arguments[i + 1]);
  }
  return s;
};
استفاده از این متد هم نتیجه درستی برای مثال آخر ارائه می‌دهد.

5. روش دیگری که کمی از دو متد قبلی سریع‌تر اجرا می‌شود (به دلیل استفاده از حلقه while) به صورت زیر است:
String.format = function () {
  var s = arguments[0],
      i = arguments.length - 1;
  while (i--) {
    s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), arguments[i + 1]);
  }
  return s;
};
این متد نیز نتیجه مشابهی ارائه می‌کند. حال به مثال زیر توجه کنید:
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "{2}", "two"));
خروجی صحیح مثال فوق باید به صورت زیر باشد:
zero:0 {2}:1 two:2
درصورتی‌که رشته‌ای که دو متد از سه متد آخر (3 و 4) به عنوان خروجی ارائه می‌دهند به‌صورت زیر است:
zero:0 two:1 two:2
برای آخرین متد که ازحلقه while (درواقع با اندیس معکوس) استفاده می‌کند (5) مثالی که خطای مورد بحث را نشان می‌دهد به صورت زیر است:
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "one", "{1}"));
که خروجی اشتباه زیر را برمی‌گرداند:
zero:0 one:1 one:2
درصورتی‌که باید مقدار زیر را برگشت دهد:
zero:0 one:1 {1}:2
دلیل رخدادن این خطا اجرای عملیات replace به صورت جداگانه و کامل برای هر توکن، از اول تا آخر برای رشته‌های replace شده جاری است که کار را خراب می‌کند.

6. برای حل مشکل بالا نیز می‌توان از یکی دیگر از امکانات دستور replace استفاده کرد که به صورت زیر است:
String.format = function () {
  var args = arguments;
  return args[0].replace(/{(\d+)}/g, function (match, number) { return args[parseInt(number) + 1]; });
};
در اینجا از قابلیت سفارشی‌سازی عملیات جایگزینی در دستور replace استفاده شده است. با استفاده از این ویژگی عملیات replace برای هر توکن جداگانه انجام می‌شود و بنابراین تغییرات اعمالی در حین عملیات تاثیر مستقیمی برای ادامه روند نخواهد گذاشت.
دقت کنید که برای بکاربردن RegExp درون دستور replace به جای تولید یک نمونه از شی RegExp می‌توان عبارت مربوطه را نیز مستقیما بکار برد. در اینجا از عبارتی کلی برای دریافت تمامی توکن‌های با فرمتی به صورت {عدد} استفاده شده است.
متد سفارشی مربوطه نیز شماره ردیف توکن یافته‌شده به همراه خود عبارت یافته‌شده را به عنوان آرگومان ورودی دریافت کرده و مقدار متناظر را از لیست آرگومان‌های متد اصلی پس از تبدیل شماره ردیف توکن به یک عدد، برگشت می‌دهد (در اینجا نیز برای جلوگیری از دورشدن از بحث اصلی، جستجو برای کسب اطلاعات بیشتر در این زمینه به خوانندگان واگذار می‌شود).
برای جلوگیری از تداخل بین آرگومان‌های متد اصلی و متد تهیه‌شده برای سفارشی‌سازی عملیات جایگزینی، در ایتدای متد اصلی، لیست آرگومان‌های آن درون متغیر جداگانه‌ای (args) ذخیره شده است.
با استفاده از این متد خروجی درست نشان داده می‌شود. حال مثال زیر را درنظر بگیرید:
console.log(String.format("{0} is {1} nice!", "donettips.info"));
خروجی این مثال به‌صورت زیر است:
donettips.info is undefined nice!
پیاده‌سازی زیر برای حل این مشکل استفاده می‌شود.

7. برای کنترل بیشتر و رفع خطاهای احتمالی در متد بالا، می‌توان ابتدا از وجود آرگومان مربوطه در متغیر args اطمینان حاصل کرد تا از جایگزینی مقدار undefined در رشته نهایی جلوگیری کرد. مانند نمونه زیر:
String.format = function () {
  var s = arguments[0],
      args = arguments;
  return s.replace(/{(\d+)}/g, function (match, number) {
    var i = parseInt(number);
    return typeof args[i + 1] != 'undefined' ? args[i + 1] : match;
  });
};
با استفاده از این متد جدید خروجی مثال‌های قبل درست خواهد بود.
در فرمت بندی رشته‌ها برای نمایش خود کاراکتر { یا } از تکرار آن‌ها (یعنی {{ یا }}) استفاده می‌شود. اما متد ما تا این لحظه این امکان را ندارد. برای مثال:
console.log(String.format("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}}  {{{{2}}}}   {2}", "zero", "{2}", "two"));
که خروجی زیر را ارائه می‌دهد:
zero:0 {2}:1 two:2, {zero} {{{2}}}  {{{two}}}   two
.
8. برای پیاده‌سازی امکان اشاره‌شده در بالا می‌توان از کد زیر استفاده کرد:
String.format = function () {
  var s = arguments[0],
      args = arguments;
  return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) {
    if (match == "{{") { return "{"; }
    if (match == "}}") { return "}"; }
    var i = parseInt(number);
    return typeof args[i + 1] != 'undefined'
                              ? args[i + 1]
                              : match;
  });
};
در اینجا با استفاده از یک عبارت RegExp پیچیده‌تر و کنترل تکرار کاراکترهای { و } در متد سفارشی جایگزینی در دستور replace، پیاده‌سازی اولیه این ویژگی ارائه شده است.
این متد خروجی صحیح زیر را برای مثال آخر ارائه می‌دهد:
zero:0 {2}:1 two:2, {0} {{2}}  {{2}}   two

پیاده‌سازی به‌صورت یک خاصیت prototype
تمامی متدهای نشان داده‌شده تا اینجا به‌صورت مستقیم از طریق String.format در دسترس خواهند بود (تعریفی شبیه به متدهای استاتیک در دات نت). درصورتی‌که بخواهیم از این متدها به صورت یک خاصیت prototype شی string استفاده کنیم (چیزی شبیه به متدهای instance در اشیای دات نت) می‌توانیم از تعریف زیر استفاده کنیم:
String.prototype.format = function () {
   ...
}
تنها فرق مهم این پیاده‌سازی این است که رشته مربوط به فرمت وارده در این متد از طریق شی this در دسترس است و بنابراین شماره اندیس آرگومان‌های متد یکی کمتر از متدهای قبلی است که باید مدنظر قرار گیرد. مثلا برای متد آخر خواهیم داشت:
String.prototype.format = function () {
  var s = this.toString(),
      args = arguments;
  return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) {
    if (match == "{{") { return "{"; }
    if (match == "}}") { return "}"; }
    return typeof args[number] != 'undefined'
                              ? args[number]
                              : match;
  });
};

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

ازآنجاکه نیاز به تغییر اندیس در متد سفارشی عملیات replace وجود ندارد، بنابراین خط مربوط به تبدیل آرگومان number به یک مقدار عددی (با دستور parseInt) حذف شده است و از این متغیر به صورت مستقیم استفاده شده است. در این حالت عملیات تبدیل توسط خود جاوا اسکریپت مدیریت می‌شود که کار را راحت‌تر می‌سازد.
بنابراین متد ما به صورت زیر قابل استفاده است:
console.log("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}}  {{{{2}}}}   {2}".format("zero", "{2}", "two"));

پیاده‌سازی با استفاده از توکن‌های غیرعددی
برای استفاده از توکن‌های غیرعددی می‌توانیم به صورت زیر عمل کنیم:
String.format = function () {
  var s = arguments[0],
      args = arguments[1];
  for (var arg in args) {
    s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]);
  }
  return s;
};
برای حالت prototype نیز داریم:
String.prototype.format = function () {
  var s = this.toString(),
      args = arguments[0];
  for (var arg in args) {
    s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]);
  }
  return s;
};
با استفاده از این دو متد داریم:
console.log(String.format("{site} is {adj}! {site} is {adj}!", { site: "donettips.info", adj: "nice" }));
console.log("{site} is {adj}! {site} is {adj}!".format({ site: "donettips.info", adj: "nice" }));
.
تا اینجا متدهایی نسبتا کامل برای نیازهای عادی برنامه‌نویسی تهیه شده است. البته کار توسعه این متد برای پشتیبانی از امکانات پیشرفته‌تر فرمت‌بندی رشته‌ها می‌تواند ادامه پیدا کند.

کتابخانه‌های موجود
یکی از کامل‌ترین کتابخانه‌های کار با رشته‌ها همان کتابخانه معروف Microsoft Ajax Client Libray است که بیشتر امکانات موجود کار با رشته‌ها در دات نت را در خود دارد. صرفا جهت آشنایی، پیاده‌سازی متد String.format در این کتابخانه در زیر آورده شده است:
String.format = function String$format(format, args) {
  /// <summary locid="M:J#String.format" />
  /// <param name="format" type="String"></param>
  /// <param name="args" parameterArray="true" mayBeNull="true"></param>
  /// <returns type="String"></returns>
//  var e = Function._validateParams(arguments, [
//    { name: "format", type: String },
//    { name: "args", mayBeNull: true, parameterArray: true }
//  ]);
//  if (e) throw e;
  return String._toFormattedString(false, arguments);
};
String._toFormattedString = function String$_toFormattedString(useLocale, args) {
  var result = '';
  var format = args[0];
  for (var i = 0; ; ) {
    var open = format.indexOf('{', i);
    var close = format.indexOf('}', i);
    if ((open < 0) && (close < 0)) {
      result += format.slice(i);
      break;
    }
    if ((close > 0) && ((close < open) || (open < 0))) {
      if (format.charAt(close + 1) !== '}') {
        throw Error.argument('format', Sys.Res.stringFormatBraceMismatch);
      }
      result += format.slice(i, close + 1);
      i = close + 2;
      continue;
    }
    result += format.slice(i, open);
    i = open + 1;
    if (format.charAt(i) === '{') {
      result += '{';
      i++;
      continue;
    }
    if (close < 0) throw Error.argument('format', Sys.Res.stringFormatBraceMismatch);
    var brace = format.substring(i, close);
    var colonIndex = brace.indexOf(':');
    var argNumber = parseInt((colonIndex < 0) ? brace : brace.substring(0, colonIndex), 10) + 1;
    if (isNaN(argNumber)) throw Error.argument('format', Sys.Res.stringFormatInvalid);
    var argFormat = (colonIndex < 0) ? '' : brace.substring(colonIndex + 1);
    var arg = args[argNumber];
    if (typeof (arg) === "undefined" || arg === null) {
      arg = '';
    }
    if (arg.toFormattedString) {
      result += arg.toFormattedString(argFormat);
    }
    else if (useLocale && arg.localeFormat) {
      result += arg.localeFormat(argFormat);
    }
    else if (arg.format) {
      result += arg.format(argFormat);
    }
    else
      result += arg.toString();
    i = close + 1;
  }
  return result;
}
دقت کنید قسمت ابتدایی این متد که برای بررسی اعتبار آرگومان‌های ورودی است، برای سادگی عملیات کامنت شده است. همان‌طور که می‌بینید این متد پیاده‌سازی نسبتا مفصلی دارد و امکانات بیشتری نیز در اختیار برنامه نویسان قرار می‌دهد. البته سایر متدهای مربوطه بدلیل طولانی بودن در اینجا آورده نشده است. برای مثال امکانات پیشرفته‌تری مثل زیر با استفاده از این کتابخانه در دسترس هستند:
console.log(String.format("{0:n}, {0:c}, {0:p}, {0:d}", 100.0001));
// result:   100.00, ¤100.00, 10,000.01 %, 100.0001

console.log(String.format("{0:d}, {0:t}", new Date(2015, 1, 1, 10, 45)));
// result:   02/01/2015, 10:45
آخرین نسخه این کتابخانه از اینجا قابل دریافت است (این متدها درون فایل MicrosoftAjax.debug.js قرار دارند). این کتابخانه دیگر به این صورت و با این نام توسعه داده نمیشود و چند سالی است که تصمیم به توسعه ویژگی‌های جدید آن به صورت پلاگین‌های jQuery گرفته شده است.

کتابخانه دیگری که می‌توان برای عملیات فرمت‌بندی رشته‌ها در جاوا اسکریپت از آن استفاده کرد، کتابخانه معروف jQuery Validation است. این کتابخانه یک متد نسبتا خوب با نام format برای فرمت کردن رشته‌ها دارد. نحوه استفاده از این متد به صورت زیر است:
var template = jQuery.validator.format("{0} is not a valid value");
console.log(template("abc"));
// result: 'abc is not a valid value'

کتابخانه نسبتا کامل دیگری که وجود دارد، با عنوان Stringformat از اینجا قابل دریافت است. برای استفاده از این کتابخانه باید به صورت زیر عمل کرد:
String.format([full format string], [arguments...]);
// or:
[date|number].format([partial format string]);
همان‌طور که می‌بینید این کتابخانه امکانات کامل‌تری نیز دارد. مثال‌های مربوط به این کتابخانه به صورت زیر هستند که توانایی‌های نسبتا کامل آن‌را نشان می‌دهد:
// Object path
String.format("Welcome back, {username}!", 
{ id: 3, username: "JohnDoe" });
// Result: "Welcome back, JohnDoe!"

// Date/time formatting
String.format("The time is now {0:t}.", 
new Date(2009, 5, 1, 13, 22));
// Result: "The time is now 01:22 PM."

// Date/time formatting (without using a full format string)
var d = new Date();
d.format("hh:mm:ss tt");
// Result: "02:28:06 PM"

// Custom number format string
String.format("Please call me at {0:+##0 (0) 000-00 00}.", 4601111111);
// Result: "Please call me at +46 (0) 111-11 11."

// Another custom number format string
String.format("The last year result was {0:+$#,0.00;-$#,0.00;0}.", -5543.346);
// Result: "The last year result was -$5,543.35."

// Alignment
String.format("|{0,10:PI=0.00}|", Math.PI);
// Result: "|   PI=3.14|"

// Rounding
String.format("1/3 ~ {0:0.00}", 1/3);
// Result: "1/3 ~ 0.33"

// Boolean values
String.format("{0:true;;false}", 0);
// Result: "false"

// Explicitly specified localization
// (note that you have to include the .js file for used cultures)
msf.setCulture("en-US");
String.format("{0:#,0.0}", 3641.667);
// Result: "3,641.7"

msf.setCulture("sv-SE");
String.format("{0:#,0.0}", 3641.667);
// Result: "3 641,7"

یک کتابخانه دیگر نیز از این آدرس قابل دریافت است. این کتابخانه با عنوان String.format نام‌گذاری شده است. نحوه استفاده از این کتابخانه نیز به صورت زیر است:
//inline arguments
String.format("some string with {0} and {1} injected using argument {{number}}", 'first value', 'second value');
//returns: 'some string with first value and second value injected argument {number}'

//single array
String.format("some string with {0} and {1} injected using array {{number}}", [ 'first value', 'second value' ]);
//returns: 'some string with first value and second value injected using array {number}'

//single object
String.format("some string with {first} and {second} value injected using {{propertyName}}",{first:'first value',second:'second value'});
//returns: 'some string with first value and second value injected using {propertyName}'
کتابخانه نسبتا معروف و کامل sprintf نیز در اینجا وجود دارد. این کتابخانه امکانات بسیاری همچون متدهای متناظر در زبان C دارد.

منابع

مطالب
ارسال مستقیم یک فایل PDF به چاپگر
برنامه رایگان Adobe reader یک سری خط فرمان دارد که توسط آن‌ها می‌توان فایل‌های PDF را مستقیما به چاپگر ارسال کرد. در ادامه قطعه کدی را ملاحظه خواهید کرد که انجام اینکار را کپسوله می‌کند:
using System;
using System.Diagnostics;
using System.IO;
using System.Management;
using Microsoft.Win32;

namespace PdfFilePrinter
{
    /// <summary>
    /// Executes the Adobe Reader and prints a file while suppressing the Acrobat print
    /// dialog box, then terminating the Reader.
    /// </summary>
    public class AcroPrint
    {
        /// <summary>
        /// The Adobe Reader or Adobe Acrobat path such as 'C:\Program Files\Adobe\Adobe Reader X\AcroRd32.exe'.
        /// If it's not specified, the InstalledAdobeReaderPath property value will be used.
        /// </summary>
        public string AdobeReaderPath { set; get; }

        /// <summary>
        /// Returns the default printer name.
        /// </summary>
        public string DefaultPrinterName
        {
            get
            {
                var query = new ObjectQuery("SELECT * FROM Win32_Printer");
                using (var searcher = new ManagementObjectSearcher(query))
                {
                    foreach (var mo in searcher.Get())
                    {
                        if (((bool?)mo["Default"]) ?? false)
                            return mo["Name"] as string;
                    }
                }
                return string.Empty;
            }
        }

        /// <summary>
        /// The name and path of the PDF file to print.
        /// </summary>
        public string PdfFilePath { set; get; }

        /// <summary>
        /// Name of the printer such as '\\PrintServer\HP LaserJet'.
        /// If it's not specified, the DefaultPrinterName property value will be used.
        /// </summary>
        public string PrinterName { set; get; }

        /// <summary>
        /// Returns the HKEY_CLASSES_ROOT\Software\Adobe\Acrobat\Exe value.
        /// If AcroRd32.exe does not exist, returns string.Empty
        /// </summary>
        public string InstalledAdobeReaderPath
        {
            get
            {
                var acroRd32Exe = Registry.ClassesRoot.OpenSubKey(@"Software\Adobe\Acrobat\Exe", writable: false);
                if (acroRd32Exe == null)
                    return string.Empty;

                var exePath = acroRd32Exe.GetValue(string.Empty) as string;
                if (string.IsNullOrEmpty(exePath))
                    return string.Empty;

                exePath = exePath.Trim(new[] { '"' });
                return File.Exists(exePath) ? exePath : string.Empty;
            }
        }

        /// <summary>
        /// Executes the Adobe Reader and prints a file while suppressing the Acrobat print
        /// dialog box, then terminating the Reader.
        /// </summary>
        /// <param name="timeout">The amount of time, in milliseconds, to wait for the associated process to exit. The maximum is the largest possible value of a 32-bit integer, which represents infinity to the operating system.</param>
        public void PrintPdfFile(int timeout = Int32.MaxValue)
        {
            if (!File.Exists(PdfFilePath))
                throw new ArgumentException(PdfFilePath + " does not exist.");

            var args = string.Format("/N /T \"{0}\" \"{1}\"", PdfFilePath, getPrinterName());
            var process = startAdobeProcess(args);
            if (!process.WaitForExit(timeout))
                process.Kill();
        }

        private Process startAdobeProcess(string arguments = "")
        {
            var startInfo = new ProcessStartInfo
                {
                    FileName = this.getExePath(),
                    Arguments = arguments,
                    CreateNoWindow = true,
                    ErrorDialog = false,
                    UseShellExecute = false,
                    Verb = "print"
                };

            return Process.Start(startInfo);
        }

        private string getPrinterName()
        {
            var printer = PrinterName;
            if (string.IsNullOrEmpty(printer))
                printer = DefaultPrinterName;

            if (string.IsNullOrEmpty(printer))
                throw new ArgumentException("Please set the PrinterName.");

            return printer;
        }

        private string getExePath()
        {
            var exePath = AdobeReaderPath;
            if (string.IsNullOrEmpty(exePath) || !File.Exists(exePath))
                exePath = InstalledAdobeReaderPath;

            if (string.IsNullOrEmpty(exePath))
                throw new ArgumentException("Please set the full path of the AcroRd32.exe or Acrobat.exe.");

            return exePath;
        }
    }
}

توضیحات:
استفاده ابتدایی از کلاس فوق به نحو زیر است:
            new AcroPrint 
            {
                PdfFilePath = @"D:\path\test.pdf"
            }.PrintPdfFile();
به این ترتیب فایل PDF ذکر شده به چاپگر پیش فرض سیستم ارسال می‌شود.

ملاحظات:
- کدهای فوق نیاز به ارجاعی به اسمبلی استاندارد System.Management.dll نیز دارند.
- اگر علاقمند بودید که چاپگر خاصی را معرفی کنید (برای مثال یک چاپگر تعریف شده در شبکه)، می‌توانید خاصیت PrinterName را مقدار دهی نمائید.
- محل نصب Adobe reader از رجیستری ویندوز استخراج می‌شود. اما اگر محل نصب برنامه استاندارد نبود، نیاز است خاصیت AdobeReaderPath مقدار دهی گردد.
- تحت هر شرایطی برنامه Adobe reader ظاهر خواهد شد؛ حتی اگر در حین آغاز پروسه سعی در مخفی کردن پنجره آن نمائید. اینکار به عمد جهت مسایل امنیتی در این برنامه درنظر گرفته شده است تا کاربر بداند که پروسه چاپ آغاز شده است.
مطالب
تعیین اعتبار یک GUID در دات نت

GUID یا Globally unique identifier یک عدد صحیح 128 بیتی است (بنابراین 2 به توان 128 حالت را می‌توان برای آن درنظر گرفت). از لحاظ آماری تولید دو GUID یکسان تقریبا صفر می‌باشد. به همین جهت از آن با اطمینان می‌توان به عنوان یک شناسه منحصربفرد استفاده کرد. برای مثال اگر به لینک‌های دانلود فایل‌ها از سایت مایکروسافت دقت کنید، این نوع GUID ها را به وفور می‌توانید ملاحظه نمائید. یا زمانیکه قرار است فایلی را که بر روی سرور آپلود شده، ذخیره نمائیم، می‌توان نام آن‌را یک GUID درنظر گرفت بدون اینکه نگران باشیم آیا فایل آپلود شده بر روی یکی از فایل‌های موجود overwrite می‌شود یا خیر. یا مثلا استفاده از آن در سناریوی بازیابی کلمه عبور در یک سایت. هنگامیکه کاربری درخواست بازیابی کلمه عبور فراموش شده خود را داد، یک GUID برای آن تولید کرده و به او ایمیل می‌زنیم و در آخر آن‌را در کوئری استرینگی دریافت کرده و با مقدار موجود در دیتابیس مقایسه می‌کنیم. مطمئن هستیم که این عبارت قابل حدس زدن نیست و همچنین یکتا است.

برای تولید GUID ها در دات نت می‌توان مانند مثال زیر عمل کرد و خروجی‌های دلخواهی را با فرمت‌های مختلفی دریافت کرد:

System.Guid.NewGuid().ToString() = 81276701-9dd7-42e9-b128-81c762a172ff
System.Guid.NewGuid().ToString("N") = 489ecfc61ee7403988efe8546806c6a2
System.Guid.NewGuid().ToString("D") = 119201d9-84d9-4126-b93f-be6576eedbfd
System.Guid.NewGuid().ToString("B") = {fd508d4b-cbaf-4f1c-894c-810169b1d20c}
System.Guid.NewGuid().ToString("P") = (eee1fe00-7e63-4632-a290-516bfc457f42)

تمام این‌ها خیلی هم خوب! اما همان سناریوی مشخص ساختن یک فایل با GUID و یا بازیابی کلمه عبور فراموش شده را درنظر بگیرید. یکی از اصول امنیتی مهم، تعیین اعتبار ورودی کاربر است. چگونه باید یک GUID را به صورت مؤثری تعیین اعتبار کرد و مطمئن شد که کاربر از این راه قصد تزریق اس کیوال را ندارد؟
دو روش برای انجام اینکار وجود دارد
الف) عبارت دریافت شده را به new Guid پاس کنیم. اگر ورودی غیرمعتبر باشد، یک exception تولید خواهد شد.
ب) استفاده از regular expressions جهت بررسی الگوی عبارت وارد شده

پیاده سازی این دو را در کلاس زیر می‌توان ملاحظه نمود:
using System;
using System.Text.RegularExpressions;

namespace sample
{
/// <summary>
/// بررسی اعتبار یک گوئید
/// </summary>
public static class CValidGUID
{
/// <summary>
/// بررسی تعیین اعتبار ورودی
/// </summary>
/// <param name="guidString">ورودی</param>
/// <returns></returns>
public static bool IsGuid(this string guidString)
{
if (string.IsNullOrEmpty(guidString)) return false;

bool bResult;
try
{
Guid g = new Guid(guidString);
bResult = true;
}
catch
{
bResult = false;
}

return bResult;
}

/// <summary>
/// بررسی تعیین اعتبار ورودی
/// </summary>
/// <param name="input">ورودی</param>
/// <returns></returns>
public static bool IsValidGUID(this string input)
{
return !string.IsNullOrEmpty(input) &&
new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$").IsMatch(input);
}
}

}

سؤال: آیا متدهای فوق ( extension methods ) درست کار می‌کنند و واقعا نیاز ما را برآورده خواهند ساخت؟ به همین منظور، آزمایش واحد آن‌ها را نیز تهیه خواهیم کرد:

using NUnit.Framework;
using sample;

namespace TestLibrary
{
[TestFixture]
public class TestCValidGUID
{

/*******************************************************************************/
[Test]
public void TestIsGuid1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsGuid());
}

[Test]
public void TestIsGuid2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsGuid());
}

[Test]
public void TestIsGuid3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsGuid());
}

[Test]
public void TestIsGuid4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsGuid());
}

[Test]
public void TestIsGuid5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsGuid());
}


/*******************************************************************************/
[Test]
public void TestIsValidGUID1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsValidGUID());
}

[Test]
public void TestIsValidGUID2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsValidGUID());
}

[Test]
public void TestIsValidGUID3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsValidGUID());
}

[Test]
public void TestIsValidGUID4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsValidGUID());
}

[Test]
public void TestIsValidGUID5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsValidGUID());
}
}

}

نتیجه این آزمایش به صورت زیر است:



همانطور که ملاحظه می‌کنید حالت دوم یعنی استفاده از عبارات باقاعده دو حالت را نمی‌تواند بررسی کند (مطابق الگوی بکار گرفته شده که البته قابل اصلاح است)، اما روش معمولی استفاده از new Guid ، تمام فرمت‌های تولید شده توسط دات نت را پوشش می‌دهد.


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


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

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


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

                List<string> propertyNames;

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

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



                UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);

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

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

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

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

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

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

                filterContext.ExceptionHandled = true;

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