مطالب
تهیه خروجی PDF و اکسل از حاصل جستجوی پویای jqGrid به کمک PDF Report
پیشنیازها
- صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
- فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC
- سفارشی سازی عناصر صفحات پویای افزودن و ویرایش رکوردهای jqGrid در ASP.NET MVC
- آشنایی با کتابخانه‌ی PDF Report


اضافه کردن دکمه‌ی خروجی به jqGrid

برای تهیه خروجی از jqGrid نیاز است بدانیم، اکنون در چه صفحه‌ای از اطلاعات قرار داریم؟ بر روی چه ستونی، مرتب سازی صورت گرفته‌است؟ بر روی کدام فیلدها با چه مقادیری جستجو انجام شده‌است؟ تا ... بتوانیم بر این مبنا، منبع داده‌ی موجود را فیلتر کرده و لیست نهایی را تبدیل به گزارش کنیم. گزارشی که دقیقا با اطلاعاتی که کاربر در صفحه مشاهده می‌کند، تطابق داشته باشد.
خوشبختانه تمام این سؤالات توسط متد توکار excelExport در سمت سرور قابل دریافت است:
@section Scripts
{
    <script type="text/javascript">
    $(document).ready(function () {
        $('#list').jqGrid({
            caption: "آزمایش ششم",
                 // مانند قبل
            }).navGrid(
                 // مانند قبل
                    }).jqGrid('navButtonAdd', '#pager', {
                        caption: "", buttonicon: "ui-icon-print", title: "خروجی پی دی اف",
                        onClickButton: function () {
                            $("#list").jqGrid('excelExport', { url: '@Url.Action("GetProducts", "Home")' });
                        }
                    });
        });
    </script>
}

در اینجا توسط متد navButtonAdd یک دکمه‌ی جدید را اضافه کرده‌ایم که کلیک بر روی آن سبب فراخوانی متد excelExport و ارسال اطلاعات گزارش به url تنظیم شده‌است. باید دقت داشت که این اطلاعات از طریق Http Get به سرور ارسال می‌شوند و دقیقا اجزای آن همان اجزای جستجوی پویای jqGrid است:
public ActionResult GetProducts(string sidx, string sord, int page, int rows,
                                             bool _search, string searchField, string searchString,
                                             string searchOper, string filters, string oper)
با این تفاوت که یک oper نیز به مجموعه‌ی پارامترهای ارسالی به سرور اضافه شده‌است. این oper در اینجا با excel مقدار دهی می‌شود.
البته چون تعداد این پارامترها بیش از اندازه شده‌است، بهتر است آن‌ها را تبدیل به یک کلاس کرد:
namespace jqGrid06.Models
{
    public class JqGridRequest
    {
        public string sidx { set; get; }
        public string sord { set; get; }
        public int page { set; get; }
        public int rows { set; get; }
        public bool _search { set; get; }
        public string searchField { set; get; }
        public string searchString { set; get; }
        public string searchOper { set; get; }
        public string filters { set; get; }
        public string oper { set; get; }
    }
}
و متد جستجوی پویا را به نحو ذیل بازنویسی نمود:
        public ActionResult GetProducts(JqGridRequest request)
        {
            var list = ProductDataSource.LatestProducts;

            var pageIndex = request.page - 1;
            var pageSize = request.rows;
            var totalRecords = list.Count;
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var productsQuery = list.AsQueryable();

            productsQuery = new JqGridSearch().ApplyFilter(productsQuery, request, this.Request.Form);
            productsQuery = productsQuery.OrderBy(request.sidx + " " + request.sord);

            if (string.IsNullOrWhiteSpace(request.oper))
            {
                productsQuery = productsQuery
                                    .Skip(pageIndex * pageSize)
                                    .Take(pageSize);
            }
            else if (request.oper == "excel")
            {
                productsQuery = productsQuery
                                    .Skip(pageIndex * pageSize);
            }

            var productsList = productsQuery.ToList();

            if (!string.IsNullOrWhiteSpace(request.oper) && request.oper == "excel")
            {
                new ProductsPdfReport().CreatePdfReport(productsList);
            }

            var productsData = new JqGridData
            {
                Total = totalPages,
                Page = request.page,
                Records = totalRecords,
                Rows = (productsList.Select(product => new JqGridRowData
                {
                    Id = product.Id,
                    RowCells = new List<string>
                    {
                        product.Id.ToString(CultureInfo.InvariantCulture),
                        product.Name,
                        product.AddDate.ToPersianDate(),
                        product.Price.ToString(CultureInfo.InvariantCulture)
                    }
                })).ToArray()
            };

            return Json(productsData, JsonRequestBehavior.AllowGet);
        }

توضیحات:
اکثر قسمت‌های این متد با متدی که در مطلب «فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC» مشاهده کردید یکی است؛ برای مثال order by آن با استفاده از کتابخانه‌ی Dynamic LINQ به صورت پویا عمل می‌کند و متد ApplyFilter، کار تهیه where پویا را انجام می‌دهد.
فقط در اینجا بررسی و پردازش پارامتر oper نیز اضافه شده‌است. اگر این پارامتر مقدار دهی شده باشد، یعنی نیاز است کل اطلاعات را واکشی کرد؛ زیرا می‌خواهیم گزارش گیری کنیم و نه اینکه صرفا اطلاعات یک صفحه را به کاربر بازگشت دهیم. همچنین در اینجا List نهایی فیلتر شده به یک گزارش Pdf Report ارسال می‌شود. این گزارش چون نهایتا اطلاعات را در مرورگر کاربر Flush می‌کند، کار به اجرای سایر قسمت‌ها نخواهد رسید و همینجا گزارش نهایی تهیه می‌شود.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid06.7z
 
مطالب
چگونگی دسترسی به فیلد و خاصیت غیر عمومی
یک از ابتدایی‌ترین مواردی که در یادگیری دات نت آموزش داده می‌شود مباحث مربوط به کپسوله سازی است. برای مثال فیلد‌ها و خواص Private که به صورت خصوصی هستند یا Protected هستند از خارج کلاس قابل دسترسی نیستند. برای دسترسی به این کلاس‌ها باید از خواص یا متدهای عمومی استفاده کرد.
public class Book
    {
        private int code = 10;        

        public int GetCode()
        {
            return code;
        }
    }
یا فیلدها و خواصی که به صورت فقط خواندنی هستند،(RealOnly) امکان تغییر مقدار برای اون‌ها وجود ندارد. برای مثال کد پایین کامپایل نخواهد شد.
public class Book
    {
        private readonly int code = 10;        

        public int GetCode()
        {
            return code = 20;
        }        
    }
اما در دات نت با استفاده از Reflection‌ها می‌تونیم تمام قوانین بالا رو نادیده بگیریم. یعنی می‌تونیم هم به خواص و فیلد‌های غیر عمومی کلاس دسترسی پیدا کنیم و هم می‌تونیم مقدار فیلدهای فقط خواندنی رو تغییر بدیم. به مثال‌های زیر دقت کنید.
#مثال اول
using System.Reflection;

 public class Book
 {
        private int code = 10;
 }

 public class Program
 {
        static void Main( string[] args )
        {
            Book book = new Book();
            var codeField = book.GetType().GetField( "code", BindingFlags.NonPublic | BindingFlags.Instance );
            codeField.SetValue( book, 20 );
            var value = codeField.GetValue( book );
        }
    }
ابتدا یک کلاس که دارای یک متغیر به نام کد است ساخته ایم که مقدار 10 را دارد. فیلد به صورت private  است. بعد از اجرا به راحتی مقدار Code را به دست می‌آوریم.


حتی امکان تغییر مقدار فیلد private هم امکان پذیر است.

#مثال دوم.
در این مثال قصد داریم مقدار یک فیلد، از نوع فقط خواندنی رو تغییر دهیم.
using System.Reflection;

 public class Book
 {
        private readonly int code = 10;
 }

 public class Program
 {
        static void Main( string[] args )
        {
            Book book = new Book();
            var codeField = book.GetType().GetField( "code", BindingFlags.NonPublic | BindingFlags.Instance );
            codeField.SetValue( book, 50);
            var value = codeField.GetValue( book );
        }
    }
بعد از اجرا مقدار متغیر code به 50 تغییر می‌یابد.

مطالب تکمیلی



مطالب
سری بررسی SQL Smell در EF Core - ایجاد روابط Polymorphic - بخش اول
سناریویی را در نظر بگیرید که برای هر کدام از مدلهای Article, Video, Event می‌خواهیم قابلیت کامنت‌گذاری جداگانه‌ای را داشته باشیم. چندین روش برای پیاده‌سازی این سناریو وجود دارد که در ادامه به آنها خواهیم پرداخت. 

Polymorphic association  
در این روش بجای تعریف چند کلید خارجی، تنها یک فیلد جنریک را تعریف خواهیم کرد که می‌تواند همزمان یک ارجاع را به مدل‌های مطرح شده داشته باشد. برای تعیین نوع کلید هم نیاز به یک فیلد دیگر جهت تعیین نوع ارجاع خواهیم داشت. در واقع با کمک آن می‌توانیم تشخیص دهیم که ارجاع موردنظر به کدام موجودیت اشاره دارد: 


public enum CommentType
{
    Article,
    Video,
    Event
}

public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    public int? TypeId { get; set; }
    public CommentType CommentType { get; set; }
}

public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
}

public class Video
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
}

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");
}

این روش در واقع به عنوان یک Anti Pattern و SQL Smell شناخته می‌شود؛ زیرا امکان کوئری گرفتن از دیتابیس را دشوار خواهد کرد. اکثر فریم‌ورک‌های غیر دات‌نتی به صورت توکار قابلیت پیاده‌سازی این نوع ارتباط را ارائه می‌دهند. اما در Entity Framework باید به صورت دستی تنظیمات انجام شوند و همچنین به دلیل نداشتن ارجاع مستقیم (کلید خارجی) درون جدول Comments با مشکل data integrity مواجه خواهیم شد. یکی دیگر از مشکلات آن امکان درج orphaned record است؛ زیرا هیچ Constraintی بر روی Polymorphic Key تعریف نشده‌است. در این روش مدیریت واکشی اطلاعات سخت خواهد بود و در حین کوئری گرفتن دیتا باید CommentType را نیز به همراه TypeId به صورت صریحی قید کنیم:
var articleComments = dbContext.Comments
                .Where(x => x.CommentType == CommentType.Article && x.TypeId.Value == 1);
foreach (var articleComment in articleComments)
{
    Console.WriteLine(articleComment.CommentText);
}

Join Table Per Relationship Type
 یک روش دیگر ایجاد Join Table به ازای هر ارتباط است:


public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    
    public virtual ICollection<ArticleComment> ArticleComments { get; set; }
    public virtual ICollection<VideoComment> VideoComments { get; set; }
    public virtual ICollection<EventComment> EventComments { get; set; }
}

public class Article
{
    public Article()
    {
        ArticleComments = new HashSet<ArticleComment>();
    }
    
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<ArticleComment> ArticleComments { get; set; }

}

public class Video
{
    public Video()
    {
        VideoComments = new HashSet<VideoComment>();
    }
    
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<VideoComment> VideoComments { get; set; }
}

public class Event
{
    public Event()
    {
        EventComments = new HashSet<EventComment>();
    }
    
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    
    public virtual ICollection<EventComment> EventComments { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<ArticleComment> ArticleComments { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<VideoComment> VideoComments { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<EventComment> EventComments { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ArticleComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.ArticleId })
                .HasName("PK_dbo.ArticleComments");

            entity.HasIndex(e => e.ArticleId)
                .HasName("IX_ArticleId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_ArticleCommentId");

            entity.HasOne(d => d.Article)
                .WithMany(p => p.ArticleComments)
                .HasForeignKey(d => d.ArticleId)
                .HasConstraintName("FK_dbo.ArticleComments_dbo.Articles_ArticleId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.ArticleComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.ArticleComments_dbo.Comments_CommentId");
        });
        
        modelBuilder.Entity<VideoComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.VideoId })
                .HasName("PK_dbo.VideoComments");

            entity.HasIndex(e => e.VideoId)
                .HasName("IX_VideoId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_VideoCommentId");

            entity.HasOne(d => d.Video)
                .WithMany(p => p.VideoComments)
                .HasForeignKey(d => d.VideoId)
                .HasConstraintName("FK_dbo.VideoComments_dbo.Videos_VideoId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.VideoComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.VideoComments_dbo.Comments_CommentId");
        });
        
        modelBuilder.Entity<EventComment>(entity =>
        {
            entity.HasKey(e => new { e.CommentId, e.EventId })
                .HasName("PK_dbo.EventComments");

            entity.HasIndex(e => e.EventId)
                .HasName("IX_EventId");

            entity.HasIndex(e => e.CommentId)
                .HasName("IX_EventCommentId");

            entity.HasOne(d => d.Event)
                .WithMany(p => p.EventComments)
                .HasForeignKey(d => d.EventId)
                .HasConstraintName("FK_dbo.EventComments_dbo.Events_EventId");

            entity.HasOne(d => d.Comment)
                .WithMany(p => p.EventComments)
                .HasForeignKey(d => d.CommentId)
                .HasConstraintName("FK_dbo.EventComments_dbo.Comments_CommentId");
        });
    }
}


همانطور که مشاهده میکنید روش فوق نیاز به اضافه کردن مدلهای بیشتری دارد و همچنین تمام روابط چند به چند نیز نیاز است به صورت کامل تنظیم شوند. مزیت این روش داشتن Constraint برای تمامی کلیدهای خارجی است؛ بنابراین می‌توانیم از صحت دیتا مطمئن شویم:
var article = new Article
{
    Title = "Article A",
    Slug = "article_a",
    Description = "No Description"
};
var comment = new Comment
{
    CommentText = "It's great",
    User = "Sirwan"
};
dbContext.ArticleComments.Add(new ArticleComment
{
    Article = article,
    Comment = comment
});

dbContext.SaveChanges();

var articleOne = dbContext.Articles
    .Include(article => article.ArticleComments)
    .ThenInclude(comment => comment.Comment)
    .First(article => article.Id == 1);
var article1Comments = articleOne.ArticleComments.Select(x => x.Comment);
Console.WriteLine(article1Comments.Count());

Exclusive Belongs To  
یک روش دیگر، اضافه کردن ارجاعی به ازای هر کدام از مدلهای عنوان شده، درون موجودیت Comment می‌باشد که به صورت nullable خواهند بود. بنابراین اگر به عنوان مثال بخواهیم برای یک Article یک کامنت داشته باشیم، کلید رکورد ذخیره شده را به عنوان کلید خارجی در جدول Comments اضافه خواهیم کرد:


public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    
    // Article
    public virtual Article Article { get; set; }
    public int? ArticleId { get; set; }
    
    // Video
    public virtual Video Video { get; set; }
    public int? VideoId { get; set; }
    
    // Event
    public virtual Event Event { get; set; }
    public int? EventId { get; set; }
}
public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class Video
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class Event
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    public virtual ICollection<Comment> Comments { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Article> Articles { get; set; }
    public DbSet<Video> Videos { get; set; }
    public DbSet<Event> Events { get; set; }
    public DbSet<Comment> Comments { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=polymorphic.db");
}


این روش از لحاظ منطقی و طراحی دیتابیس بدون اشکال است؛ زیرا مقدار نامعتبری را نمی‌توانیم برای کلیدهای خارجی درج کنیم. چون برای کلیدهای تعریف شده درون جدول Comment یکسری Constraint تعریف شده‌اند که صحت دیتای ورودی را بررسی خواهند کرد. حتی در صورت نیاز نیز می‌توانیم یک Constraint ترکیبی را جهت مطمئن شدن از خالی نبودن همزمان ستون‌های FK اضافه کنیم. البته SQLite Provider از HasCheckConstraint پشتیبانی نمی‌کند، ولی اگر به عنوان مثال از MySQL استفاده می‌کنید می‌توانید Constraint موردنظر را اینگونه اضافه کنید: 
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Comment>(entity =>
        entity.HasCheckConstraint("CHECK_FKs", 
            "(`ArticleId`  IS NOT NULL) AND (`VideoId`  IS NOT NULL) AND (`EventId`  IS NOT NULL)"));
}

با طراحی فوق می‌توانیم مطمئن شویم که orphaned record نخواهیم داشت. اما اگر تعداد مدل‌ها بیشتر شوند، باید به ازای هر مدل جدید، یک ارجاع به آن را به جدول Comment اضافه کنیم که در نهایت با تعداد زیادی کلیدهای خارجی مواجه خواهیم شد که در آن واحد فقط یکی از آنها مقدار دارند و بقیه NULL خواهند شد. در مقابل، مزیت این روش، امکان کوئری نویسی ساده‌ی آن است:
var articles = dbContext.Articles
                .Include(x => x.Comments).Where(x => x.Id == 1);
foreach (var article in articles)
{
    Console.WriteLine($"{article.Title} - Comments: {article.Comments.Count}");
}
var comment = dbContext.Comments.Include(x => x.Article)
    .FirstOrDefault(x => x.Id == 1);
Console.WriteLine(comment?.Article.Title);

کدهای مطلب جاری را می‌توانید از اینجا دریافت کنید (هر مثال بر روی برنچی جدا قرار دارد)
نظرات مطالب
معرفی System.Text.Json در NET Core 3.0.
سلام. توی net5 کانورترش اگر عددی داخل کوتیشن باشه و مدلمون هم به صورت عددی باشه (byte, int,...) بایند میکنه ولی اگر داخل کوتیشن خالی باشه دیگه بایند نمیشه مثلاً به byte? (نالیبل)
برای این موضوع راه حلی هست؟
public class SampleModel
{
public byte? Value { get; set; }
}

var options = new JsonSerializerOptions();
options.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString;

var body = @"{""Value"":""""}";
var model = JsonSerializer.Deserialize<SampleModel>(body, options);

مطالب دوره‌ها
پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
در این قسمت قصد داریم با نحوه پیاده سازی امتیاز دهی ستاره‌ای به مطالب، که نمونه‌ای از آن‌را در سایت جاری در قسمت‌های مختلف آن مشاهده می‌کنید، آشنا شویم.


مدل برنامه

در ابتدای کار نیاز است تا ساختاری را جهت ارائه لیستی از مطالب که دارای گزینه امتیاز دهی می‌باشند، تهیه کنیم:
namespace jQueryMvcSample03.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        /// <summary>
        /// اطلاعات رای گیری یک مطلب به صورت یک خاصیت تو در تو یا پیچیده
        /// </summary>
        public Rating Rating { set; get; }

        public BlogPost()
        {
            Rating = new Rating();
        }
    }
}

namespace jQueryMvcSample03.Models
{
    //[ComplexType]
    public class Rating
    {
        public double? TotalRating { get; set; }
        public int? TotalRaters { get; set; }
        public double? AverageRating { get; set; }
    }
}
اگر با EF Code first آشنا باشید، خاصیت Rating تعریف شده در اینجا می‌تواند از نوع ComplexType تعریف شود که شامل جمع امتیازهای داده شده، تعداد کل رای دهنده‌ها و همچنین میانگین امتیازهای حاصل است.


منبع داده فرضی برنامه

using System.Collections.Generic;
using System.Linq;
using jQueryMvcSample03.Models;

namespace jQueryMvcSample03.DataSource
{
    /// <summary>
    /// منبع داده فرضی
    /// </summary>
    public static class BlogPostDataSource
    {
        private static IList<BlogPost> _cachedItems;
        /// <summary>
        /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانی‌ها صورت خواهد گرفت
        /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای
        /// تهیه یک لیست برای تمام کاربران سایت است
        /// </summary>
        static BlogPostDataSource()
        {
            _cachedItems = createBlogPostsInMemoryDataSource();
        }

        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>        
        private static IList<BlogPost> createBlogPostsInMemoryDataSource()
        {
            var results = new List<BlogPost>();
            for (int i = 1; i < 30; i++)
            {
                results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i, Rating = new Rating { TotalRaters = i + 1, AverageRating = 3.5 } });
            }
            return results;
        }

        /// <summary>
        /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند
        /// شماره صفحه از یک شروع می‌شود
        /// </summary>
        public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4)
        {
            var skipRecords = pageNumber * recordsPerPage;
            return _cachedItems
                        .OrderByDescending(x => x.Id)
                        .Skip(skipRecords)
                        .Take(recordsPerPage)
                        .ToList();
        }
    }
}
در این مثال نیز از یک منبع داده فرضی تشکیل شده در حافظه استفاده خواهیم کرد تا امکان اجرای پروژه پیوستی را بدون نیاز به بانک اطلاعاتی خاصی و بدون نیاز به مقدمات برپایی آن، به سادگی داشته باشید.
در این منبع داده ابتدا لیستی از مطالب تهیه شده و سپس کش می‌شوند. در ادامه توسط متد GetLatestBlogPosts بازه‌ای از این اطلاعات قابل بازیابی خواهند بود که برای استفاده در حالات صفحه بندی اطلاعات بهینه سازی شده است.


آشنایی با طراحی افزونه jQuery Star Rating

افزودن CSS نمایش امتیازها در ذیل هر مطلب

/* star rating system */
.post_rating
{
direction: ltr;
}
.rating
{
text-indent: -99999px;
overflow: hidden;
background-repeat: no-repeat;
display: inline-block;
width: 8px;
height: 16px;
}
.rating.stars
{
background-image: url('Images/star_rating.png');
}
.rating.stars.active
{
cursor: pointer;
}
.star-left_off
{
background-position: -0px -0px;
}
.star-left_on
{
background-position: -16px -0px;
}
.star-right_off
{
background-position: -8px -0px;
}
.star-right_on
{
background-position: -24px -0px;
}
برای نمایش ستاره‌ها و کار با تصویر Images/star_rating.png (که در پروژه پیوست قرار دارد) ابتدا نیاز است CSS فوق را به پروژه خود اضافه نمائید.

افزودن افزونه jQuery Star rating

// <![CDATA[
(function ($) {
    $.fn.StarRating = function (options) {
        var defaults = {            
            ratingStarsSpan: '.rating.stars',
            postInfoUrl: '/',
            loginUrl: '/login',
            errorHandler: null,
            completeHandler: null,
            onlyOneTimeHandler: null
        };
        var options = $.extend(defaults, options);

        return this.each(function () {
            var ratingStars = $(this);

            $(ratingStars).unbind('mouseover');
            $(ratingStars).mouseover(function () {
                var span = $(this).parent("span");
                var newRating = $(this).attr("value");
                setRating(span, newRating);
            });

            $(ratingStars).unbind('mouseout');
            $(ratingStars).mouseout(function () {
                var span = $(this).parent("span");
                var rating = span.attr("rating");
                setRating(span, rating);
            });

            $(ratingStars).unbind('click');
            $(ratingStars).click(function () {
                var span = $(this).parent("span");
                var newRating = $(this).attr("value");
                var text = span.children("span");
                var pID = span.attr("post");
                var type = span.attr("sectiontype");
                postData({ postID: pID, rating: newRating, sectionType: type });
                span.attr("rating", newRating);
                setRating(span, newRating);
            });

            function setRating(span, rating) {
                span.find(options.ratingStarsSpan).each(function () {
                    var value = parseFloat($(this).attr("value"));
                    var imgSrc = $(this).attr("class");
                    if (value <= rating)
                        $(this).attr("class", imgSrc.replace("_off", "_on"));
                    else
                        $(this).attr("class", imgSrc.replace("_on", "_off"));
                });
            }

            function postData(dataJsonArray) {
                $.ajax({
                    type: "POST",
                    url: options.postInfoUrl,
                    data: JSON.stringify(dataJsonArray),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = options.loginUrl;
                        }
                        else if (status === 'error' || !data) {
                            if (options.errorHandler)
                                options.errorHandler(this);
                        }
                        else if (data == "nok") {
                            if (options.onlyOneTimeHandler)
                                options.onlyOneTimeHandler(this);
                        }
                        else {
                            if (options.completeHandler)
                                options.completeHandler(this);
                        }
                    }
                });
            }
        });
    };
})(jQuery);
// ]]>
اطلاعات فوق، فایل jquery.StarRating.js را تشکیل می‌دهند که باید به پروژه اضافه گردند.
کاری که این افزونه انجام می‌دهد ردیابی حرکت ماوس بر روی ستاره‌های نمایش داده شده و سپس ارسال سه پارامتر ذیل به اکشن متدی که توسط پارامتر postInfoUrl مشخص می‌گردد، پس از کلیک کاربر می‌باشد:
 { postID: pID, rating: newRating, sectionType: type }
همانطور که ملاحظه می‌کنید به ازای هر قطعه رای گیری که به صفحه اضافه می‌شود، Id مطلب، رای داده شده و نام قسمت جاری، به اکشن متدی خاص ارسال خواهند گردید. sectionType از این جهت اضافه گردیده است تا بتوانید با بیش از یک جدول کار کنید و از این افزونه در قسمت‌های مختلف سایت به سادگی بتوانید استفاده نمائید.
در اینجا از errorHandler برای نمایش خطاها، از completeHandler برای نمایش تشکر به کاربر و از onlyOneTimeHandler برای نمایش اخطار مثلا «یکبار بیشتر مجاز نیستید به ازای یک مطلب رای دهید»، می‌توان استفاده کرد.

بنابراین تا اینجا فایل layout برنامه تقریبا چنین مداخلی را خواهد داشت:
<head>
    <title>@ViewBag.Title</title>    
    <link href="@Url.Content("Content/starRating.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.StarRating.js")" type="text/javascript"></script>
    @RenderSection("JavaScript", required: false)
</head>

طراحی یک HTML helper برای نمایش ستاره‌های امتیاز دهی

ابتدا پوشه استاندارد app_code را به پروژه اضافه کرده و سپس فایلی را به نام StarRatingHelper.cshtml، با محتوای ذیل به آن اضافه نمائید:
@using System.Globalization
@helper AddStarRating(int postId,
                      double? average = 0, int? postRatingsCount = 0, string type = "BlogPost",
                      string tooltip = "لطفا جهت رای دادن کلیک نمائید")
    {
        string actIt = "active ";
        if (!average.HasValue) { average = 0; }
        if (!postRatingsCount.HasValue) { postRatingsCount = 0; }
    
    <span class='postRating' rating='@average' post='@postId' title='@tooltip' sectiontype='@type'>
        @for (double i = .5; i <= 5.0; i = i + .5)
        {
            string left;
            if (i <= average)
            {
                left = (i * 2) % 2 == 1 ? "left_on" : "right_on";
            }
            else
            {
                left = (i * 2) % 2 == 1 ? "left_off" : "right_off";
            }
            <span class='rating stars @(actIt)star-@left' value='@i'></span>
        }
        &nbsp;
        @if (postRatingsCount > 0)
        {
            var ratingInfo = string.Format(CultureInfo.InvariantCulture, "امتیاز {0:0.00} از 5 توسط {1} نفر", average, postRatingsCount);
            <span>@ratingInfo</span>                
        }
        else
        {
            <span></span>
        }
    </span>
}
از این Html helper برای تشکیل ساختار نمایش قطعه امتیاز دهی به یک مطلب استفاده خواهیم کرد که توسط افزونه جی‌کوئری فوق ردیابی می‌شود.


کنترلر ذخیره سازی اطلاعات دریافتی برنامه

using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample03.DataSource;
using jQueryMvcSample03.Security;

namespace jQueryMvcSample03.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var postsList = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0);
            return View(postsList); //نمایش صفحه اصلی
        }


        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult SaveRatings(int? postId, double? rating, string sectionType)
        {
            if (postId == null || rating == null || string.IsNullOrWhiteSpace(sectionType))
                return Content(null); //اعلام بروز خطا

            if (!this.HttpContext.CanUserVoteBasedOnCookies(postId.Value, sectionType))
                return Content("nok"); //اعلام فقط یکبار مجاز هستید رای دهید

            switch (sectionType) //قسمت‌های مختلف سایت که در جداول مختلفی قرار دارند نیز می‌توانند گزینه امتیاز دهی داشته باشند
            {
                case "BlogPost":
                    //الان شماره مطلب و رای ارسالی را داریم که می‌توان نسبت به ذخیره آن اقدام کرد
                    //مثلا
                    //_blogPostsService.SaveRating(postId.Value, rating.Value);
                    break;

                //... سایر قسمت‌های دیگر سایت

                default:
                    return Content(null); //اعلام بروز خطا
            }

            return Content("ok"); //اعلام موفقیت آمیز بودن ثبت اطلاعات
        }

        [HttpGet]
        public ActionResult Post(int? id)
        {
            if (id == null)
                return Redirect("/");

            //todo: show the content here
            return Content("Post " + id.Value);
        }
    }
}
در اینجا کنترلری را که کار پردازش کلیک کاربر را بر روی امتیازی خاص انجام می‌دهد، ملاحظه می‌کنید.
امضای اکشن متد SaveRatings دقیقا بر اساس سه پارامتر ارسالی توسط jquery.StarRating.js که پیشتر توضیح داده شد، تعیین گردیده است. در این متد ابتدا بررسی می‌شود که آیا اطلاعاتی دریافت شده است یا خیر. اگر خیر، null را بازگشت خواهد داد. سپس توسط متد CanUserVoteBasedOnCookies بررسی می‌شود که آیا کاربر می‌تواند (خصوصا مجددا) رای دهد یا خیر. این افزونه برای رای دهی کاربران وارد نشده به سیستم نیز مناسب است. به همین جهت از کوکی‌ها برای ثبت اطلاعات رای دادن کاربران استفاده گردیده است. پیاده سازی متد CanUserVoteBasedOnCookies را در ادامه ملاحظه خواهید نمود.
در ادامه در متد SaveRatings، یک switch تشکیل شده است تا بر اساس نام قسمت مرتبط به رای گیری، اطلاعات را بتوان به سرویس خاصی در برنامه هدایت کرد. مثلا اطلاعات قسمت مطالب به سرویس مطالب و قسمت نظرات به سرویس نظرات هدایت شوند.


متدهایی برای کار با کوکی‌ها در ASP.NET MVC

using System;
using System.Web;

namespace jQueryMvcSample03.Security
{
    public static class CookieHelper
    {
        public static bool CanUserVoteBasedOnCookies(this HttpContextBase httpContext, int postId, string sectionType)
        {
            string key = sectionType + "-" + postId;
            var value = httpContext.GetCookieValue(key);
            if (string.IsNullOrWhiteSpace(value))
            {
                httpContext.AddCookie(key, key);
                return true;
            }
            return false;
        }

        public static void AddCookie(this HttpContextBase httpContextBase, string cookieName, string value)
        {
            httpContextBase.AddCookie(cookieName, value, DateTime.Now.AddDays(30));
        }

        public static void AddCookie(this HttpContextBase httpContextBase, string cookieName, string value, DateTime expires)
        {
            var cookie = new HttpCookie(cookieName)
            {
                Expires = expires,
                Value = httpContextBase.Server.UrlEncode(value) // For Cookies and Unicode characters
            };
            httpContextBase.Response.Cookies.Add(cookie);
        }

        public static string GetCookieValue(this HttpContextBase httpContext, string cookieName)
        {
            var cookie = httpContext.Request.Cookies[cookieName];
            if (cookie == null)
                return string.Empty; //cookie doesn't exist

            // For Cookies and Unicode characters
            return httpContext.Server.UrlDecode(cookie.Value);
        }
    }
}
در اینجا یک سری متد الحاقی را ملاحظه می‌کنید که برای ثبت اطلاعات رای داده شده یک کاربر بر اساس Id مطلب و نام قسمت متناظر با آن در یک کوکی طراحی شده‌اند. بدیهی است اگر تمام قسمت‌های برنامه شما محافظت شده هستند و کاربران حتما نیاز است ابتدا به سیستم لاگین نمایند، می‌توانید این قسمت را حذف کرده و اطلاعات postId و SectionType را به ازای هر کاربر، جداگانه در بانک اطلاعاتی ثبت و بازیابی نمائید (دقیق‌ترین حالت ممکن؛ البته برای سیستمی بسته که حتما تمام قسمت‌های آن نیاز به اعتبار سنجی دارند).


پیشنهادی در مورد نحوه ذخیره سازی اطلاعات دریافتی

using jQueryMvcSample03.Models;

namespace jQueryMvcSample03.DataSource
{
    public interface IBlogPostsService
    {
        void SaveRating(int postId, double rating);
    }

    public class SampleService : IBlogPostsService
    {
        /// <summary>
        /// یک نمونه از متد ذخیره سازی اطلاعات پیشنهادی
        /// فقط برای ایده گرفتن
        /// بدیهی است محل قرارگیری اصلی آن در لایه سرویس برنامه شما خواهد بود
        /// </summary>
        public void SaveRating(int postId, double rating)
        {
            BlogPost post = null;
            //post = _blogCtx.Find(postId); // بر اساس شماره مطلب، مطلب یافت شده و فیلدهای آن تنظیم می‌شوند
            if (post == null) return;

            if (!post.Rating.TotalRaters.HasValue) post.Rating.TotalRaters = 0;
            if (!post.Rating.TotalRating.HasValue) post.Rating.TotalRating = 0;
            if (!post.Rating.AverageRating.HasValue) post.Rating.AverageRating = 0;

            post.Rating.TotalRaters++;
            post.Rating.TotalRating += rating;
            post.Rating.AverageRating = post.Rating.TotalRating / post.Rating.TotalRaters;

            // todo: call save changes at the end.
        }
    }
}
همانطور که عنوان شد، سه داده Id مطلب، رای داده شده و نام قسمت متناظر به اکشن متد ارسال می‌شود. از نام قسمت، برای انتخاب سرویس ذخیره سازی اطلاعات استفاده خواهیم کرد. این سرویس می‌تواند شامل متدی به نام SaveRating، همانند کدهای فوق باشد که Id مطلب و عدد رای حاصل به آن ارسال می‌گردند. ابتدا بر اساس این Id، مطلب متناظر یافت شده و سپس اطلاعات Rating آن به روز خواهد شد. در پایان هم ذخیره سازی اطلاعات باید صورت گیرد.



Viewهای برنامه

قسمت پایانی کار ما در اینجا تهیه دو View است:
الف) یک Partial view که لیست مطالب را به همراه گزینه رای دهی به آن‌ها رندر می‌کند.
ب) View کاملی که از این Partial View استفاده کرده و همچنین افزونه jquery.StarRating.js را فراخوانی می‌کند.
@using System.Text.RegularExpressions
@model IList<jQueryMvcSample03.Models.BlogPost>
<ul>
    @foreach (var item in Model)
    {
        <li>
            <fieldset>
            <legend>مطلب @item.Id</legend>
                <h5>
                    @Html.ActionLink(linkText: item.Title,
                                 actionName: "Post",
                                 controllerName: "Home",
                                 routeValues: new { id = item.Id },
                                 htmlAttributes: null)
                </h5>
                @item.Body
                <div class="post_rating">
                    @Html.Raw(Regex.Replace(@StarRatingHelper.AddStarRating(item.Id, item.Rating.AverageRating, item.Rating.TotalRaters, "BlogPost").ToHtmlString(), @">\s+<", "><"))
                </div>
            </fieldset>
        </li>
    }
</ul>
کدهای _ItemsList.cshtml را در اینجا ملاحظه می‌کند که در آن نحوه فراخوانی متد کمکی StarRatingHelper.AddStarRating ذکر شده است.
اگر به کدهای آن دقت کنید از Regex.Replace برای حذف فاصله‌های خالی و خطوط جدید بین تگ‌ها استفاده گردیده است. اگر اینکار انجام نشود، نیمه‌های ستاره‌های نمایش داده شده، با فاصله از یکدیگر رندر می‌شوند که صورت خوشایندی ندارد.

و نهایتا View ایی که از این اطلاعات استفاده می‌کنید ساختار زیر را خواهد داشت:
@model IList<jQueryMvcSample03.Models.BlogPost>
@{
    ViewBag.Title = "Index";
    var postInfoUrl = Url.Action(actionName: "SaveRatings", controllerName: "Home");
}
<h2>
    سیستم امتیاز دهی</h2>
@{ Html.RenderPartial("_ItemsList", Model); }
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $(".rating.stars.active").StarRating({
                ratingStarsSpan: '.rating.stars',
                postInfoUrl: '@postInfoUrl',
                loginUrl: '/login',
                errorHandler: function () {
                    alert('خطایی رخ داده است');
                },
                completeHandler: function () {
                    alert('با تشکر! رای شما با موفقیت ثبت شد');
                },
                onlyOneTimeHandler: function () {
                    alert('فقط یکبار می‌توانید به ازای هر مطلب رای دهید');
                }
            });
        });
    </script>
}
در این View لیستی از مطالب دریافت و به partial view طراحی شده برای نمایش ارسال می‌شود. سپس افزونه StarRating نیز تنظیم و به صفحه اضافه خواهد گردید. نکته مهم آن تعیین صحیح اکشن متدی است که قرار است اطلاعات را دریافت کند و نحوه مقدار دهی آن‌را توسط متغیر postInfoUrl مشاهده می‌کنید.

دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample03.zip
مطالب
ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF
طی این  مقاله، نحوه‌ی ذخیره سازی تنظیمات متغیر و پویای یک برنامه را به صورت Strongly Typed ارائه خواهم داد. برای این منظور، یک API را که از Lazy Loading ، Cache ، Reflection و Entity Framework بهره میگیرد، خواهیم ساخت.
برنامه‌ی هدف ما که از این API استفاده می‌کند، یک اپلیکیشن Asp.net MVC است. قبل از شروع به ساخت API مورد نظر، یک دید کلی در مورد آنچه که قرار است در نهایت توسعه یابد، در زیر مشاهده میکنید:
public SettingsController(ISettings settings)
{
  // example of saving 
  _settings.General.SiteName = "دات نت تیپس";
  _settings.Seo.HomeMetaTitle = ".Net Tips";
  _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection";
  _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";
  _settings.Save();
}

همانطور که در کدهای بالا مشاهده میکنید، شی setting_ ما دارای دو پراپرتی فقط خواندنی بنام‌های General و Seo است که شامل  تنظیمات مورد نظر ما هستند و این دو کلاس از کلاس پایه‌ی SettingBase ارث بری کرده‌اند. دو دلیل برای انجام این کار وجود دارد:
  1. تنظیمات به صورت گروه بندی شده در کنار  هم قرار گرفته‌اند و یافتن تنظیمات برای زمانی که نیاز به دسترسی  به آنها داریم، راحت‌تر و ساده‌تر خواهد بود. 
  2. به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.

اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟ 

شاید فکر کنید چرا باید تنظیمات را در دیتابیس ذخیره کنیم در حالی که فایل web.config در درسترس است و می‌توان توسط کلاس ConfigurationManager به اطلاعات آن دسترسی داشت.
جواب: دلیل این است که با تغییر فایل web.config، برنامه‌ی وب شما ری استارت خواهد شد (چه زمان‌هایی یک برنامه Asp.net ری استارت میشود).
برای جلوگیری از این مساله، راه حل مناسب برای ذخیره سازی اطلاعاتی که نیاز به تغییر در زمان اجرا دارند، استفاده از از دیتابیس می‌باشد. در این مقاله از Entity Framework و پایگاه داده Sql Sever استفاده می‌کنم.

مراحل ساخت Setting API مورد نظر به شرح زیر است:
  1. ساخت یک Asp.net Web Application 
  2. ساخت مدل Setting و افزودن آن به کانتکست Entity Framework 
  3. ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
  4. ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کرده‌اند.
  5. ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات 

یک برنامه‌ی Asp.Net Web Application را از نوع MVC ایجاد کنید. تا اینجا مرحله‌ی اول ما به پایان رسید؛ چرا که ویژوال استودیو کار‌های مورد نیاز ما را انجام خواهد داد.
 لازم است مدل خود را به ApplicationDbContext موجود در فایل IdentityModels.cs معرفی کنیم. به شکل زیر:
namespace DynamicSettingAPI.Models
{
    public interface IUnitOfWork
    {
        DbSet<Setting> Settings { get; set; }
        int SaveChanges();
    }
} 

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork
    {
        public DbSet<Setting> Settings { get; set; }
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }


namespace DynamicSettingAPI.Models
{
    public class Setting
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string Value { get; set; }
    }
}
مدل تنظیمات ما خیلی ساده است و دارای سه پراپرتی به نام‌های Name ، Type ، Value هست که به ترتیب برای دریافت مقدار تنظیمات، نام کلاسی که از کلاس SettingBase ارث برده و نام تنظیمی که لازم داریم ذخیره کنیم، در نظر گرفته شده‌اند. 
لازم است تا متد OnModelCreating مربوط به ApplicationDbContext را نیز تحریف کنیم تا کانفیگ مربوط به مدل خود را نیز اعمال نمائیم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Setting>()
                    .HasKey(x => new { x.Name, x.Type });

            modelBuilder.Entity<Setting>()
                        .Property(x => x.Value)
                        .IsOptional();

            base.OnModelCreating(modelBuilder);
        }
ساختاری به شکل زیر مد نظر ماست:

  کلاس SettingBase ما همچین ساختاری را خواهد داشت:
namespace DynamicSettingAPI.Service
{
    public abstract class SettingsBase
    {
        //1
        private readonly string _name;
        private readonly PropertyInfo[] _properties;

        protected SettingsBase()
        {
            //2
            var type = GetType();
            _name = type.Name;
            _properties = type.GetProperties();
        }

        public virtual void Load(IUnitOfWork unitOfWork)
        {
            //3 get setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                //get the setting from setting list
                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    //4 set 
                    propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
                }
            }
        }
        public virtual void Save(IUnitOfWork unitOfWork)
        {
            //5 get all setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                var propertyValue = propertyInfo.GetValue(this, null);
                var value = (propertyValue == null) ? null : propertyValue.ToString();

                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    // 6 update existing value
                    setting.Value = value;
                }
                else
                {
                    // 7 create new setting
                    var newSetting = new Setting()
                    {
                        Name = propertyInfo.Name,
                        Type = _name,
                        Value = value,
                    };
                    unitOfWork.Settings.Add(newSetting);
                }
            }
        }
    }
}
این کلاس قرار است توسط کلاس‌های تنظیمات ما به ارث برده شود و در واقع کارهای مربوط به رفلکشن را در این کلاس کپسوله کرده‌ایم. همانطور که مشخص است ما دو فیلد را به نام‌های name_ و properties_ به صورت فقط خواندنی در نظر گرفته ایم که نام کلاس مورد نظر ما که از این کلاس به ارث خواهد برد، به همراه پراپرتی‌های آن، در این ظرف‌ها قرار خواهند گرفت.
متد Load وظیفه‌ی واکشی تمام تنظیمات مربوط به Type و ست کردن مقادیر به دست آمده را به خصوصیات کلاس ما، برعهده دارد. کد زیر مقدار دریافتی از دیتابیس را به نوع داده پراپرتی مورد نظر تبدیل کرده و نتیجه را به عنوان Value پراپرتی ست میکند. 
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
متد Save نیز وظیفه‌ی ذخیره سازی مقادیر موجود در خصوصیات کلاس تنظیماتی را که از کلاس SettingBase ما به ارث برده است، به عهده دارد. 
این متد دیتا‌های موجود دردیتابیس را که متعلق به کلاس ارث برده مورد نظر ما هستند، واکشی میکند و در یک حلقه، اگر خصوصیتی در دیتابیس موجود بود، آن را ویرایش کرده وگرنه یک رکورد جدید را ثبت میکند.

  کلاس‌های تنظیمات شخصی سازی شده خود را به شکل زیر تعریف میکنیم :
  public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
        public bool RegisterUsersEnabled { get; set; }
    }

 public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
    }
نیازی به توضیح ندارد.
برای اینکه تنظیمات را به صورت یکجا داشته باشیم و Abstraction ای را برای استفاده از این API ارائه دهیم، یک اینترفیس و یک کلاس که اینترفیس مذکور را پیاده کرده است در نظر میگیریم: 
public interface ISettings
{
    GeneralSettings General { get; }
    SeoSettings Seo { get; }
    void Save();
}

public class Settings : ISettings
{
    // 1
    private readonly Lazy<GeneralSettings> _generalSettings;
    // 2
    public GeneralSettings General { get { return _generalSettings.Value; } }

    private readonly Lazy<SeoSettings> _seoSettings;
    public SeoSettings Seo { get { return _seoSettings.Value; } }

    private readonly IUnitOfWork _unitOfWork;
    public Settings(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        // 3
        _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>);
        _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>);
    }

    public void Save()
    {
        // only save changes to settings that have been loaded
        if (_generalSettings.IsValueCreated)
            _generalSettings.Value.Save(_unitOfWork);

        if (_seoSettings.IsValueCreated)
            _seoSettings.Value.Save(_unitOfWork);

        _unitOfWork.SaveChanges();
    }
    // 4
    private T CreateSettings<T>() where T : SettingsBase, new()
    {
        var settings = new T();
        settings.Load(_unitOfWork);
        return settings;
    }
}
این اینترفیس مشخص می‌کند که ما به چه نوع تنظیماتی، دسترسی داریم و متد Save آن برای آپدیت کردن تنظیمات، در نظر گرفته شده است. هر کلاسی که از کلاس SettingBase ارث بری کرده را به صورت فیلد فقط خواندنی و با استفاده از کلاس Lazy درون آن ذکر میکنیم و به این صورت کلاس تنظیمات ما زمانی ساخته خواهد شد که برای اولین بار به آن دسترسی داشته باشیم.
متد CreateSetting وظیفه‌ی لود دیتا را از دیتابیس، بر عهده دارد که برای این منظور، متد لود Type مورد نظر را فراخوانی میکند. این متد وقتی به کلاس تنظیمات مورد نظر برای اولین بار دسترسی پیدا کنیم، فراخوانی خواهد شد.

 حتما امکان این وجود دارد که شما از امکان Caching هم بهره ببرید برای مثال همچین متد و سازنده‌ای را در کلاس Settings در نظر بگیرید:
private readonly ICache _cache;
public Settings(IUnitOfWork unitOfWork, ICache cache)
{
    // ARGUMENT CHECKING SKIPPED FOR BREVITY
    _unitOfWork = unitOfWork;
    _cache = cache;
    _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>);
    _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>);
}

private T CreateSettingsWithCache<T>() where T : SettingsBase, new()
{
    // this is where you would implement loading from ICache
    throw new NotImplementedException();
}
در آخر هم به شکل زیر میتوان (به عنوان دمو فقط ) از این API استفاده کرد.
   public ActionResult Index()
        {
            using (var uow = new ApplicationDbContext())
            {
                var _settings = new Settings(uow);
                _settings.General.SiteName = "دات نت تیپس";
                _settings.General.AdminEmail = "admin@gmail.com";
                _settings.General.RegisterUsersEnabled = true;
                _settings.Seo.HomeMetaTitle = ".Net Tips";
                _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection";
                _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";

                var settings2 = new Settings(uow);
                var output = string.Format("SiteName: {0} HomeMetaDescription: {1}  MetaKeywords:  {2}  MetaTitle:  {3}  RegisterEnable:  {4}",
                    settings2.General.SiteName,
                    settings2.Seo.HomeMetaDescription,
                    settings2.Seo.MetaKeywords,
                    settings2.Seo.HomeMetaTitle,
                    settings2.General.RegisterUsersEnabled.ToString()
                    );
                return Content(output);
            }

        }

خروجی :

نکته: در پروژه ای که جدیدا در سایت ارائه داده‌ام و در حال تکمیل آن هستم، از بهبود یافته‌ی این مقاله استفاده می‌شود. حتی برای اسلاید شو‌های سایت هم میشود از این روش استفاده کرد و از فرمت json بهره برد برای این منظور. حتما در پروژه‌ی مذکور همچین امکانی را هم در نظر خواهم گرفتم.
پیشنها میکنم سورس SmartStore را بررسی کنید. آن هم به شکل مشابهی ولی پیشرفته‌تر از این مقاله، همچین امکانی را دارد.
مطالب
نگاشت دیتای XML به کمک AutoMapper
صورت مساله که مشخصه قراره دیتای رو از منبع داده‌ی Xml به model مورد نظرمون نگاشت کنیم چیزی شبیه کاری که متد GetEntries انجام میده و تو این پست معرفی شده...

AutoMapper به صورت داخلی و با استفاده از قرارداد‌ها نمیتونه xml رو به object تبدیل کنه ولی این کار به کمک LINQ to XML قابل انجامه.

مثالی که برای این پست انتخاب شده سوژه‌ی داغ روزهای اخیره ؟!
مدل زیر رو در نظر داشته باشید
 public class PreciousMetal
    {
        public string Name { get; set; }
        public float Price { get; set; }
        public DateTime UpdateTime { get; set; }
    }

قراره از یک وب سرویس اطلاعات مربوط به فلزات گرانبها رو دریافت و به مدل PreciousMetal نگاشت کنیم.ساختار اطلاعات دریافتی ما به شکل زیره
<pricelist currency="usd">
  <price timestamp="1349347920" per="ozt" commodity="gold">1788.70</price>
  <price timestamp="1349347860" per="ozt" commodity="palladium">665.50</price>
  <price timestamp="1349347920" per="ozt" commodity="platinum">1701.25</price>
  <price timestamp="1349347920" per="ozt" commodity="silver">34.91</price>
</pricelist>

برای نگاشت‌های معمولی کار سختی نداریم و از MapFrom استفاده میکنیم مثلا برای قیمت
Mapper.CreateMap<XElement, PreciousMetal>().ForMember(des => des.Price,
                                                                  op =>
                                                                  op.MapFrom(src => src.Value));

ولی برای زمان دریافت قیمت با توجه به متفاوت بودن زمان دریافتی مثلا در اینجا Unix time از Custom value resolvers استفاده میکنیم
public class UnixTimestampResolver : ValueResolver<XElement, DateTime>
    {
        protected override DateTime ResolveCore(XElement source)
        {
            var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return origin.AddSeconds(Convert.ToDouble(source.Attribute("timestamp").Value));
        }
    }

همچنیا میخواهیم از معادل فارسی نام فلزات گرانبها استفاده کنیم
public class EnglishPMetalToFarsiResolver : ValueResolver<XElement, string>
    {
        readonly Dictionary<string, string> _pMetaldic = new Dictionary<string, string>
                                                       {
                                                           {"gold", "طلا"},
                                                           {"palladium", "پالادیوم"},
                                                           {"platinum", "پلاتین "},
                                                           {"silver", "نقره"}
                                                       };
        protected override string ResolveCore(XElement source)
        {
            string pMetalFarsi;
            return _pMetaldic.TryGetValue(source.Attribute("commodity").Value, out pMetalFarsi) ? pMetalFarsi : string.Empty;
        }
    }

نکته:از سری قبلی آشنایی با AutoMapper همیشه بین انتخاب Custom Value Formatters و Custom value resolvers مشکل داشتم مثلا همین قسمت بنظر خودم Custom Value Formatters مناسبتر میاد بعد کمی وقت گذاشتن مشخص شد گویا یه جورایی Custom Value Formatters اضافه س و اشتباه تو طراحی بوده.

و اما نحوه استفاده
static void Main(string[] args)
        {
            //تعریف نگاشت‌ها Mapper.CreateMap<XElement, PreciousMetal>().ForMember(des => des.Name,
                                                                  op => op.ResolveUsing<EnglishPMetalToFarsiResolver>())
                .ForMember(des => des.Price,
                           op =>
                           op.MapFrom(src => src.Value))

                .ForMember(des => des.UpdateTime, op => op.ResolveUsing<UnixTimestampResolver>());

            Mapper.AssertConfigurationIsValid();

            //دریافت قیمت‌ها از منبع داده
            var doc = XDocument.Load("http://www.xmlcharts.com/cache/precious-metals.xml");
            var priceData = doc.Descendants("pricelist").Take(1).Elements("price");

            //فراخوانی نگاشت
            var preciousMetals = Mapper.Map<IEnumerable<XElement>, IList<PreciousMetal>>(priceData);


            foreach (var preciousMetal in preciousMetals)
            {
                Console.WriteLine(preciousMetal.Name + " " + preciousMetal.Price + " " + preciousMetal.UpdateTime.ToShortDateString());
            }

            Console.ReadLine();

        }

نظرات مطالب
یکدست کردن "ی" و "ک" در ASP.NET MVC با پیاده‌سازی یک Model Binder
یک نکته تکمیلی :
در این روش تنها پارامترهای رشته ای که به طور مستقیم، متغیر در اکشن متد تعریف میشود بایند میگردند ولی خصوصیت‌های رشته ای در پارامترهایی که از نوع کلاس تعریف شده اند که بسیار پرکاربرد هم هستند تاثیری ندارد، برای این موارد بایندر جدیدی را به شکل زیر مینویسیم:
public class StringModelBinder : DefaultModelBinder
    {

protected override void SetProperty(ControllerContext controllerContext,
          ModelBindingContext bindingContext,
          System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
        {
            if (propertyDescriptor.PropertyType == typeof(string))
            {
                var stringValue = (string)value;
                if (!string.IsNullOrEmpty(stringValue))
                {
                    value =value.ToString().Replace((char)1610, (char)1740).Replace((char)1603, (char)1705);
                }
                else
                {
                    value = null;
                }
            }

            base.SetProperty(controllerContext, bindingContext,
                                propertyDescriptor, value);
        }

    }
در اینجا متد دیگری را به نام SetProperty برای تغییر خصوصیت هر متدی که یافت میشود صدا زده میشود را رونویسی کردیم. شی propertyDescriptor  اطلاعات خصوصیت مورد نظر از قبیل نام و مقدار و ... را نگهداری میکندو در اینجا مقدار خصوصیت را دریافت کرده و تغییرات لازم را انجام میدهیم سپس مقدا تغییر یافته را توسط base به متد پدر منتقل میکنیم. سپس برای اعمال این بایندر کد زیر را اضافه میکنیم:
 ModelBinders.Binders.DefaultBinder = new StringModelBinder();
بدین ترتیب بایندر پیش فرض به سیستم معرفی میگردد.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 20 - بررسی تغییرات فیلترها
نکته تکمیلی
در صورتیکه قبل از فراخوانی delegate زیر
await next();
بخواید کاری انجام بدید که اکشن متود فراخوانی نشه نباید next رو فراخوانی کنید، چون با خطا مواجه میشوید؛ برای مثال اکشن فیلتری رو نوشتید که اگه آی پی کاربر جزء آی پی‌های بن شده سایت بود صفحه غیر مجاز رو به اون نمایش بده
public class CheckUserIp : IAsyncActionFilter
{
    private readonly string[] _bannedIps;

    public CheckUserIp(params string[] bannedIps)
    {
        _bannedIps = bannedIps;
    }
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var userIp = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
        if (_bannedIps.Contains(userIp))
        {
            context.Result = new ViewResult()
            {
                ViewName = "Forbidden"
            };
        }
        else
        {
            await next();
        }
        // await next(); => throw exception
    }
}

در مثال بالا در صورتیکه آی پی کاربر بن شده باشد دیگر next فراخوانی نمیشود و از بروز خطا جلوگیری میکند.
نحوه فراخوانی اکشن فیلتر بالا
[TypeFilter(typeof(CheckUserIp), Arguments = new object[]{ new[] { "::1", "134.56.110.44" } })]
public IActionResult Index()
{
    return View();
}
مطالب
C# 6 - Null-conditional operators
برنامه نویس‌‌های سی‌شارپ پیشتر با null-coalescing operator یا ?? آشنا شده بودند. برای مثال
 string data = null;
var result = data ?? "value";
در این حالت اگر data یا سمت چپ عملگر، نال باشد، مقدار value (سمت راست عملگر) بازگشت داده خواهد شد؛ که در حقیقت خلاصه شده‌ی چند سطر ذیل است:
if (data == null)
{
    data = "value";
}
var result = data;
در سی شارپ 6، جهت تکمیل عملگرهای کار با مقادیر نال و بالا بردن productivity برنامه نویس‌ها، عملگر دیگری به نام Null-conditional operator و یا .? به این مجموعه اضافه شده‌است. در این حالت ابتدا مقدار سمت چپ عملگر بررسی خواهد شد. اگر مقدار آن مساوی نال بود، در همینجا کار خاتمه یافته و نال بازگشت داده می‌شود. در غیر اینصورت کار بررسی زنجیره‌ی جاری ادامه خواهد یافت.
برای مثال بسیاری از نتایج بازگشتی از متدها، چند سطحی هستند:
class Response
{
    public string Result { set; get; }
    public int Code { set; get; }
}

 
class WebRequest
{
    public Response GetDataFromWeb(string url)
    {
        // ...
        return new Response { Result = null };
    }
}
در اینجا روش مرسوم کار با کلاس درخواست اطلاعات از وب به صورت ذیل است:
 var webData = new WebRequest().GetDataFromWeb("https://www.dntips.ir/");
if (webData != null && webData.Result != null)
{
    Console.WriteLine(webData.Result);
}
چون می‌خواهیم به خاصیت Result دسترسی پیدا کنیم، نیاز است دو مرحله وضعیت خروجی متد و همچنین خاصیت Result آن‌را جهت مشخص سازی نال نبودن آن‌ها، بررسی کنیم و اگر برای مثال خاصیت Result نیز خود متشکل از یک کلاس دیگر بود که در آن برای مثال StatusCode نیز ذکر شده بود، این بررسی به سه سطح یا بیشتر نیز ادامه پیدا می‌کرد.
در این حالت اگر اشاره‌گر را به محل && انتقال دهیم، افزونه‌ی ReSharper پیشنهاد یکی کردن این بررسی‌ها را ارائه می‌دهد:


به این ترتیب تمام چند سطح بررسی نال، به یک عبارت بررسی .? دار، خلاصه خواهد شد:
 if (webData?.Result != null)
{
    Console.WriteLine(webData.Result);
}
در اینجا ابتدا بررسی می‌شود که آیا webData نال است یا خیر؟ اگر نال بود همینجا کار خاتمه پیدا می‌کند و به بررسی Result نمی‌رسد. اگر نال نبود، ادامه‌ی زنجیره تا به انتها بررسی می‌شود.
البته باید دقت داشت که برای تمام سطوح باید از .? استفاده کرد (برای مثال response?.Results?.Status)؛ در غیر اینصورت همانند سابق در صورت استفاده‌ی از دات معمولی، به یک null reference exception می‌رسیم.


کار با متدها و Delegates

این عملگر جدید مقایسه‌ی با نال را بر روی متدها (علاوه بر خواص و فیلدها) نیز می‌توان بکار برد. برای مثال خلاصه شده‌ی فراخوانی ذیل:
 if (x != null)
{
   x.Dispose();
}
با استفاده از Null Conditional Operator به این صورت است:
 x?.Dispose();

و یا بکار گیری آن بر روی delegates (روش قدیمی):
 var copy = OnMyEvent;
if (copy != null)
{
   copy(this, new EventArgs());
}
نیز با استفاده از متد Invoke به نحو ذیل قابل انجام است و نکته جالب یک سطر کد ذیل علاوه بر ساده شدن آن:
 OnMyEvent?.Invoke(this, new EventArgs());
Thread-safe بودن آن نیز می‌باشد. زیرا در این حالت کامپایلر delegate را به یک متغیر موقتی کپی کرده و سپس فراخوانی‌ها را انجام می‌دهد. اگر انجام این کپی موقت صورت نمی‌گرفت، در حین فراخوانی آن از طریق چندین ترد مختلف، ممکن بود یکی از مشترکین delegate از آن قطع اشتراک می‌کرد و در این حالت فراخوانی تردی دیگر در همان لحظه، سبب کرش برنامه می‌شد.


استفاده از Null Conditional Operator بر روی Value types

الف) مقایسه با نال
کد ذیل را درنظر بگیرید:
 var code = webData?.Code;
در اینجا Code یک value type از نوع int است. در این حالت با بکارگیری Null Conditional Operator، خروجی این حاصل، از نوع <Nullable<int و یا ?int درنظر گرفته خواهد شد و با توجه به اینکه عبارات null>0 و همچنین null<0 هر دو false هستند، مقایسه‌ی این خروجی با 0 بدون مشکل انجام می‌شود. برای مثال مقایسه‌ی ذیل از نظر کامپایلر یک عبارت معتبر است و بدون مشکل کامپایل می‌شود:
 if (webData?.Code > 0)
{

}

ب) بازگشت مقدار پیش فرض دیگری بجای نال
اگر نیاز بود بجای null مقدار پیش فرض دیگری را بازگشت دهیم، می‌توان از null-coalescing operator سابق استفاده کرد:
 int count = response?.Results?.Count ?? 0;
در این مثال خاصیت CountT در اصل از نوع int تعریف شده‌است؛ اما بکارگیری .? سبب Nullable شدن آن خواهد شد. بنابراین امکان بکارگیری عملگر ?? یا null-coalescing operator نیز بر روی این متغیر وجود دارد.

ج) دسترسی به مقدار Value یک متغیر nullable
نمونه‌ی دیگر آن قطعه کد ذیل است:
 int? x = 10;
//var value = x?.Value; // invalid
Console.WriteLine(x?.ToString());
در اینجا برخلاف متغیر Code که از ابتدا nullable تعریف نشده‌است، متغیر x نال پذیر است. اما باید دقت داشت که با تعریف .? دیگر نیازی به استفاده از خاصیت Value این متغیر nullable نیست؛ زیرا .? سبب محاسبه و بازگشت خروجی آن می‌شود. بنابراین در این حالت، سطر دوم غیرمعتبر است (کامپایل نمی‌شود) و سطر سوم معتبر.


کار با indexer property و بررسی نال

اگر به عنوان بحث دقت کرده باشید، یک s جمع در انتهای Null-conditional operators ذکر شده‌است. به این معنا که این عملگر مقایسه‌ی با نال، صرفا یک شکل و فرم .? را ندارد. مثال ذیل در حین کار با آرایه‌ها و لیست‌ها بسیار مشاهده می‌شود:
 if (response != null && response.Results != null && response.Results.Addresses != null
  && response.Results.Addresses[0] != null && response.Results.Addresses[0].Zip == "63368")
{

}
در اینجا به علت بکارگیری indexer بر روی Addresses، دیگر نمی‌توان از عملگر .? که صرفا برای فیلدها، خواص، متدها و delegates طراحی شده‌است، استفاده کرد. به همین منظور، عملگر بررسی نال دیگری به شکل […]? برای این بررسی طراحی شده‌است:
 if(response?.Results?.Addresses?[0]?.Zip == "63368")
{

}
به این ترتیب 5 سطح بررسی نال فوق، به یک عبارت کوتاه کاهش می‌یابد.

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

خوب، عملگر .? کار مقایسه‌ی با نال را خصوصا در دسترسی‌های چند سطحی به خواص و متدها بسیار ساده می‌کند. اما آیا باید در همه جا از آن استفاده کرد؟ آیا باید از این پس کلا استفاده از دات را فراموش کرد و بجای آن از .? در همه جا استفاده کرد؟
مثال ذیل را درنظر بگیرید:
 public void DoSomething(Customer customer)
{
    string address = customer?.Employees
                  ?.SingleOrDefault(x => x.IsAdmin)?.Address?.ToString();
    SendPackage(address);
}
در این مثال در تمام سطوح آن از .? بجای دات استفاده شده‌است و بدون مشکل کامپایل می‌شود. اما این نوع فراخوانی سبب خواهد شد تا یک سری از مشکلات موجود کاملا مخفی شوند؛ خصوصا اعتبارسنجی‌ها. برای مثال در این فراخوانی اگر مشتری نال باشد یا اگر کارمندانی را نداشته باشد، آدرسی بازگشت داده نمی‌شود. بنابراین حداقل دو سطح بررسی و اعتبارسنجی عدم وجود مشتری یا عدم وجود کارمندان آن در اینجا مخفی شده‌اند و دیگر مشخص نیست که علت بازگشت نال چه بوده‌است.
روش بهتر انجام اینکار، بررسی وضعیت customer و انتقال مابقی زنجیره‌ی LINQ به یک متد مجزای دیگر است:
 public void DoSomething(Customer customer)
{
   Contract.Requires(customer != null); 
   string address = customer.GetAdminAddress();
   SendPackage(address);
}