مطالب
نگاشت IDictionary در Fluent NHibernate

نگاشت خودکار مجموعه‌ها در Fluent NHibernate ساده است و نیاز به تنظیم خاصی ندارد. برای مثال IList به صورت خودکار به Bag ترجمه می‌شود و الی آخر.
البته شاید سؤال بپرسید که این Bag از کجا آمده؟ کلا 6 نوع مجموعه در NHibernate پشتیبانی می‌شوند که شامل Array، Primitive-Array ، Bag ، Set ، List و Map هستند؛ این‌ اسامی هم جهت حفظ سازگاری با جاوا تغییر نکرده‌اند و گرنه معادل‌های آن‌ها در دات نت به این شرح هستند:
Bag=IList
Set=Iesi.Collections.ISet
List=IList
Map=IDictionary

البته در دات نت 4 ، ISet هم به صورت توکار اضافه شده، اما NHibernate از مدت‌ها قبل آن‌را از کتابخانه‌ی Iesi.Collections به عاریت گرفته است. مهم‌ترین تفاوت‌های این مجموعه‌ها هم در پذیرفتن یا عدم پذیرش اعضای تکراری است. Set و Map اعضای تکراری نمی‌پذیرند.
در ادامه می‌خواهیم طرز کار با Map یا همان IDictionary دات نت را بررسی کنیم:

الف) حالتی که نوع کلید و مقدار (در یک عضو Dictionary تعریف شده)، Entity نیستند
using System.Collections.Generic;

namespace Test1.Model12
{
public class User
{
public virtual int Id { set; get; }
public virtual string Name { get; set; }
public virtual IDictionary<string, string> Preferences { get; set; }
}
}

نحوه تعریف نگاشت که مبتنی است بر مشخص سازی تعاریف کلید و مقدار آن جهت تشکیل یک Map یا همان Dictionary :
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;

namespace Test1.Model12
{
public class UserMapping : IAutoMappingOverride<User>
{
public void Override(AutoMapping<User> mapping)
{
mapping.Id(x => x.Id);
mapping.HasMany(x => x.Preferences)
.AsMap<string>("FieldKey")
.Element("FieldValue", x => x.Type<string>().Length(500));
}
}
}

خروجی SQL متناظر:
create table "User" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (Id)
)

create table Preferences (
User_id INT not null,
FieldValue NVARCHAR(500) null,
FieldKey NVARCHAR(255) not null,
primary key (User_id, FieldKey)
)

alter table Preferences
add constraint FKD6CB18523B1FD789
foreign key (User_id)
references "User"

ب) حالتی که مقدار، Entity است
using System.Collections.Generic;

namespace Test1.Model13
{
public class User
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IDictionary<string, Property> Properties { get; set; }
}

public class Property
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Value { get; set; }
public virtual User User { get; set; }
}
}

نحوه تعریف نگاشت:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;

namespace Test1.Model13
{
public class UserMapping : IAutoMappingOverride<User>
{
public void Override(AutoMapping<User> mapping)
{
mapping.Id(x => x.Id);
mapping.HasMany<Property>(x => x.Properties)
.AsMap<string>("FieldKey")
.Component(x => x.Map(c => c.Id));
}
}
}
خروجی SQL متناظر:
create table "Property" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
Value NVARCHAR(255) null,
User_id INT null,
primary key (Id)
)

create table "User" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (Id)
)

create table Properties (
User_id INT not null,
Id INT null,
FieldKey NVARCHAR(255) not null,
primary key (User_id, FieldKey)
)

alter table "Property"
add constraint FKF9F4D85A3B1FD7A2
foreign key (User_id)
references "User"

alter table Properties
add constraint FK63646D853B1FD7A2
foreign key (User_id)
references "User"

ج) حالتی که کلید، Entity است
using System;
using System.Collections.Generic;

namespace Test1.Model14
{
public class FormData
{
public virtual int Id { get; set; }
public virtual DateTime? DateTime { get; set; }
public virtual IDictionary<FormField, string> FormPropertyValues { get; set; }
}

public class FormField
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
}
}

نحوه تعریف نگاشت:
using FluentNHibernate.Automapping;
using FluentNHibernate.Automapping.Alterations;

namespace Test1.Model14
{
public class FormDataMapping : IAutoMappingOverride<FormData>
{
public void Override(AutoMapping<FormData> mapping)
{
mapping.Id(x => x.Id);
mapping.HasMany<FormField>(x => x.FormPropertyValues)
.AsEntityMap("FieldId")
.Element("FieldValue", x => x.Type<string>().Length(500))
.Cascade.All();
}
}
}
خروجی SQL متناظر:
create table "FormData" (
Id INT IDENTITY NOT NULL,
DateTime DATETIME null,
primary key (Id)
)

create table FormPropertyValues (
FormData_id INT not null,
FieldValue NVARCHAR(500) null,
FieldId INT not null,
primary key (FormData_id, FieldId)
)

create table "FormField" (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (Id)
)

alter table FormPropertyValues
add constraint FKB807B9C090849E
foreign key (FormData_id)
references "FormData"

alter table FormPropertyValues
add constraint FKB807B97165898A
foreign key (FieldId)
references "FormField"

یک مثال عملی:
امکانات فوق جهت طراحی قسمت ثبت اطلاعات یک برنامه «فرم ساز» مبتنی بر Key-Value بسیار مناسب هستند؛ برای مثال:
برنامه‌ای را در نظر بگیرید که می‌تواند تعدادی خدمات داشته باشد که توسط مدیر برنامه قابل اضافه شدن است؛ برای نمونه خدمات درخواست نصب نرم افزار، خدمات درخواست تعویض کارت پرسنلی، خدمات درخواست مساعده، خدمات ... :
public class Service
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<ServiceFormField> Fields { get; set; }
public virtual IList<ServiceFormData> Forms { get; set; }
}

برای هر خدمات باید بتوان یک فرم طراحی کرد. هر فرم هم از یک سری فیلد خاص آن خدمات تشکیل شده است. برای مثال:
public class ServiceFormField
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual bool IsRequired { get; set; }
public virtual Service Service { get; set; }
}

در اینجا نیازی نیست به ازای هر فیلد جدید واقعا یک فیلد متناظر به دیتابیس اضافه شود و ساختار آن تغییر کند (برخلاف حالت dynamic components که پیشتر در مورد آن بحث شد).
اکنون با داشتن یک خدمات و فیلدهای پویای آن که توسط مدیربرنامه تعریف شده‌اند، می‌توان اطلاعات وارد کرد. مهم‌ترین نکته‌ی آن هم IDictionary تعریف شده است که حاوی لیستی از فیلدها به همراه مقادیر وارد شده توسط کاربر خواهد بود:
public class ServiceFormData
{
public virtual int Id { get; set; }
public virtual IDictionary<ServiceFormField, string> FormPropertyValues { get; set; }
public virtual DateTime? DateTime { get; set; }
public virtual Service Service { get; set; }
}

در مورد نحوه نگاشت آن هم در حالت «ج» فوق توضیح داده شد.

مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
در مورد طراحی Self Referencing Entities پیشتر مطلبی را در این سایت مطالعه کرده‌اید .
یک مثال دیگر آن می‌تواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمی‌کند؛ اما ... زمانیکه نوبت به نمایش آن فرا می‌رسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

namespace EFGeneral
{
    public class BlogComment
    {
        public int Id { set; get; }

        [MaxLength]
        public string Body { set; get; }

        public virtual BlogComment Reply { set; get; }
        public int? ReplyId { get; set; }
        public ICollection<BlogComment> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<BlogComment> BlogComments { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Self Referencing Entity
            modelBuilder.Entity<BlogComment>()
                        .HasOptional(x => x.Reply)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ReplyId)
                        .WillCascadeOnDelete(false);

            base.OnModelCreating(modelBuilder);
        }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var comment1 = new BlogComment { Body = "نظر من این است که" };
            var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 };
            var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 };

            context.BlogComments.Add(comment121);
            base.Seed(context);
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var ctx = new MyContext())
            {
                var list = ctx.BlogComments
                          //.where ...
                          .ToList() // fills the childs list too
                          .Where(x => x.Reply == null) // for TreeViewHelper                        
                          .ToList();

                if (list.Any())
                {

                }
            }
        }
    }
}

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


یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.

اولین نکته اضافه‌‌تری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر می‌باشد:
    public class BlogComment
    {
        // ... 
        public ICollection<BlogComment> Children { get; set; }
    }
این خاصیت تاثیری در نحوه تشکیل جدول ندارد. علت تعریف آن به توانمندی EF در پرکردن خودکار آن بر می‌گردد.
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب می‌شود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت می‌شود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشه‌ها فیلتر می‌شوند (مواردی که reply آن‌ها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:


سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آن‌ها در یک برنامه ASP.NET MVC می‌توان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
private void AppendChildren(TagBuilder parentTag, T parentItem, Func<T, IEnumerable<T>> childrenProperty)
        {
            var children = childrenProperty(parentItem);
            if (children == null || !children.Any())
            {
                return;
            }
//...

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

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

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

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

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

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

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

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

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

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

return feedQuery.ToList();
}
}

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

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

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

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

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

مطالب
EF Code First #14

ردیابی تغییرات در EF Code first

EF از DbContext برای ذخیره اطلاعات مرتبط با تغییرات موجودیت‌های تحت کنترل خود کمک می‌گیرد. این نوع اطلاعات توسط Change Tracker API جهت بررسی وضعیت فعلی یک شیء، مقادیر اصلی و مقادیر تغییر کرده آن در دسترس هستند. همچنین در اینجا امکان بارگذاری مجدد اطلاعات موجودیت‌ها از بانک اطلاعاتی جهت اطمینان از به روز بودن آن‌ها تدارک دیده شده است. ساده‌ترین روش دستیابی به این اطلاعات، استفاده از متد context.Entry می‌باشد که یک وهله از موجودیتی خاص را دریافت کرده و سپس به کمک خاصیت State خروجی آن، وضعیت‌هایی مانند Unchanged یا Modified را می‌توان به دست آورد. علاوه بر آن خروجی متد context.Entry، دارای خواصی مانند CurrentValues و OriginalValues نیز می‌باشد. OriginalValues شامل مقادیر خواص موجودیت درست در لحظه اولین بارگذاری در DbContext برنامه است. CurrentValues مقادیر جاری و تغییر یافته موجودیت را باز می‌گرداند. به علاوه این خروجی امکان فراخوانی متد GetDatabaseValues را جهت بدست آوردن مقادیر جدید ذخیره شده در بانک اطلاعاتی نیز ارائه می‌دهد. ممکن است در این بین، خارج از Context جاری، اطلاعات بانک اطلاعاتی توسط کاربر دیگری تغییر کرده باشد. به کمک GetDatabaseValues می‌توان به این نوع اطلاعات نیز دست یافت.
حداقل چهار کاربرد عملی جالب را از اطلاعات موجود در Change Tracker API می‌توان مثال زد که در ادامه به بررسی آن‌ها خواهیم پرداخت.


کلاس‌های مدل مثال جاری

در اینجا یک رابطه many-to-one بین جدول هزینه‌های اقلام خریداری شده یک شخص و جدول فروشندگان تعریف شده است:

using System;

namespace EF_Sample09.DomainClasses
{
public abstract class BaseEntity
{
public int Id { get; set; }

public DateTime CreatedOn { set; get; }
public string CreatedBy { set; get; }

public DateTime ModifiedOn { set; get; }
public string ModifiedBy { set; get; }
}
}

using System;

namespace EF_Sample09.DomainClasses
{
public class Bill : BaseEntity
{
public decimal Amount { set; get; }
public string Description { get; set; }

public virtual Payee Payee { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample09.DomainClasses
{
public class Payee : BaseEntity
{
public string Name { get; set; }

public virtual ICollection<Bill> Bills { set; get; }
}
}


به علاوه همانطور که ملاحظه می‌کنید، این کلاس‌ها از یک abstract class به نام BaseEntity مشتق شده‌اند. هدف از این کلاس پایه تنها تامین یک سری خواص تکراری در کلاس‌های برنامه است و هدف از آن، مباحث ارث بری مانند TPH، TPT و TPC نیست.
به همین جهت برای اینکه این کلاس پایه تبدیل به یک جدول مجزا و یا سبب یکی شدن تمام کلاس‌ها در یک جدول نشود، تنها کافی است آن‌را به عنوان DbSet معرفی نکنیم و یا می‌توان از متد Ignore نیز استفاده کرد:

using System.Data.Entity;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class Sample09Context : MyDbContextBase
{
public DbSet<Bill> Bills { set; get; }
public DbSet<Payee> Payees { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();

base.OnModelCreating(modelBuilder);
}
}
}



الف) به روز رسانی اطلاعات Context در صورتیکه از متد context.Database.ExecuteSqlCommand مستقیما استفاده شود

در قسمت قبل با متد context.Database.ExecuteSqlCommand برای اجرای مستقیم عبارات SQL بر روی بانک اطلاعاتی آشنا شدیم. اگر این متد در نیمه کار یک Context فراخوانی شود، به معنای کنار گذاشتن Change Tracker API می‌باشد؛ زیرا اکنون در سمت بانک اطلاعاتی اتفاقاتی رخ داده‌اند که هنوز در Context جاری کلاینت منعکس نشده‌اند:

using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;

namespace EF_Sample09
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());

using (var db = new Sample09Context())
{
var payee = new Payee { Name = "فروشگاه سر کوچه" };
var bill = new Bill { Amount = 4900, Description = "یک سطل ماست", Payee = payee };
db.Bills.Add(bill);

db.SaveChanges();
}

using (var db = new Sample09Context())
{
var bill1 = db.Bills.Find(1);
bill1.Description = "ماست";

db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
Console.WriteLine(bill1.Description);

db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
Console.WriteLine(bill1.Description);

db.SaveChanges();
}
}
}
}

در این مثال ابتدا دو رکورد به بانک اطلاعاتی اضافه می‌شوند. سپس توسط متد db.Bills.Find، اولین رکورد جدول Bills بازگشت داده می‌شود. در ادامه، خاصیت توضیحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نیز بار دیگر خاصیت توضیحات اولین رکورد به روز خواهد شد.
اکنون اگر مقدار bill1.Description را بررسی کنیم، هنوز دارای مقدار پیش از فراخوانی db.Database.ExecuteSqlCommand می‌باشد، زیرا تغییرات سمت بانک اطلاعاتی هنوز به Context مورد استفاده منعکس نشده است.
در اینجا برای هماهنگی کلاینت با بانک اطلاعاتی، کافی است متد Reload را بر روی موجودیت مورد نظر فراخوانی کنیم.



ب) یکسان سازی ی و ک اطلاعات رشته‌ای دریافتی پیش از ذخیره سازی در بانک اطلاعاتی

یکی از الزامات برنامه‌های فارسی، یکسان سازی ی و ک دریافتی از کاربر است. برای این منظور باید پیش از فراخوانی متد SaveChanges نهایی،‌ مقادیر رشته‌ای کلیه موجودیت‌ها را یافته و به روز رسانی کرد:

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class MyDbContextBase : DbContext
{
public void RejectChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
entry.State = EntityState.Unchanged;
break;

case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}

public override int SaveChanges()
{
applyCorrectYeKe();
auditFields();
return base.SaveChanges();
}

private void applyCorrectYeKe()
{
//پیدا کردن موجودیت‌های تغییر کرده
var changedEntities = this.ChangeTracker
.Entries()
.Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);

foreach (var item in changedEntities)
{
if (item.Entity == null) continue;

//یافتن خواص قابل تنظیم و رشته‌ای این موجودیت‌ها
var propertyInfos = item.Entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));

var pr = new PropertyReflector();

//اعمال یکپارچگی نهایی
foreach (var propertyInfo in propertyInfos)
{
var propName = propertyInfo.Name;
var val = pr.GetValue(item.Entity, propName);
if (val != null)
{
var newVal = val.ToString().Replace("ی", "ی").Replace("ک", "ک");
if (newVal == val.ToString()) continue;
pr.SetValue(item.Entity, propName, newVal);
}
}
}
}

private void auditFields()
{
// var auditUser = User.Identity.Name; // in web apps
var auditDate = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
// Note: You must add a reference to assembly : System.Data.Entity
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedOn = auditDate;
entry.Entity.ModifiedOn = auditDate;
entry.Entity.CreatedBy = "auditUser";
entry.Entity.ModifiedBy = "auditUser";
break;

case EntityState.Modified:
entry.Entity.ModifiedOn = auditDate;
entry.Entity.ModifiedBy = "auditUser";
break;
}
}
}
}
}


اگر به کلاس Context مثال جاری که در ابتدای بحث معرفی شد دقت کرده باشید به این نحو تعریف شده است (بجای DbContext از MyDbContextBase مشتق شده):
public class Sample09Context : MyDbContextBase
علت هم این است که یک سری کد تکراری را که می‌توان در تمام Contextها قرار داد، بهتر است در یک کلاس پایه تعریف کرده و سپس از آن ارث بری کرد.
تعاریف کامل کلاس MyDbContextBase را در کدهای فوق ملاحظه می‌کنید.
در اینجا کار با تحریف متد SaveChanges شروع می‌شود. سپس در متد applyCorrectYeKe کلیه موجودیت‌های تحت نظر ChangeTracker که تغییر کرده باشند یا به آن اضافه شده‌ باشند، یافت شده و سپس خواص رشته‌ای آن‌ها جهت یکسانی سازی ی و ک، بررسی می‌شوند.


ج) ساده‌تر سازی به روز رسانی فیلدهای بازبینی یک رکورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعیت جاری یک موجودیت

در کلاس MyDbContextBase فوق، کار متد auditFields، مقدار دهی خودکار خواص تکراری تاریخ ایجاد، تاریخ به روز رسانی، شخص ایجاد کننده و شخص تغییر دهنده یک رکورد است. به کمک ChangeTracker می‌توان به موجودیت‌هایی از نوع کلاس پایه BaseEntity دست یافت. در اینجا اگر entry.State آن‌ها مساوی EntityState.Added بود، هر چهار خاصیت یاد شده به روز می‌شوند. اگر حالت موجودیت جاری، EntityState.Modified بود، تنها خواص مرتبط با تغییرات رکورد به روز خواهند شد.
به این ترتیب دیگر نیازی نیست تا در حین ثبت یا ویرایش اطلاعات برنامه نگران این چهار خاصیت باشیم؛ زیرا به صورت خودکار مقدار دهی خواهند شد.


د) پیاده سازی قابلیت لغو تغییرات در برنامه

علاوه بر این‌ها در کلاس MyDbContextBase، متد RejectChanges نیز تعریف شده است تا بتوان در صورت نیاز، حالت موجودیت‌های تغییر کرده یا اضافه شده را به حالت پیش از عملیات، بازگرداند.



اشتراک‌ها
مدل بایندر برای تاریخ شمسی در ASP.NET Core

مدل بایندر برای تاریخ شمسی

How To Use:

//In Startup.cs:
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvcCore(options =>
        {
            options.ModelBinderProviders.Insert(0, new PersianDateModelBinderProvider());
        });
    }

//In Actions:
    
     public async Task<IActionResult> SampleAction(DateTime birthDate)
        {
            if (ModelState.IsValid)
            {
                //birthDate is valid
            }
            else
            {
                birthDate is wrong
            }

            ...
        }
مدل بایندر برای تاریخ شمسی در ASP.NET Core
مطالب
PersianDateTime جایگزینی برای System.DateTime
همانطور که در توضیح پروژه PersianDateTime آمده است، کلاس PersianDateTime جایگزینی است برای System.DateTime برای استفاده در پروژه‌هایی که احتیاج به تاریخ شمسی و ساعت رسمی ایران یا سایر کشورهای فارسی‌زبان، مستقل از Time Zone سیستم و در نظر گرفتن Daylight Saving Time، دارند. این کلاس شامل اکثر متدها، پراپرتی‌ها و عملگرهای متداول  System.DateTime است.

دسترسی به تاریخ و ساعت فعلی :
PersianDateTime now = PersianDateTime.Now;

string persianDateTime = now.ToString(); // 1392/03/09 23:37:57
string persianDate = now.ToString(PersianDateTimeFormat.Date); // 1392/03/09
string persianFullDateTime = now.ToString("dddd d MMMM yyyy ساعت hh:mm:ss tt"); // پنج شنبه 9 خرداد 1392 ساعت 11:37:57 ب.ظ

TimeSpan persianTime = now.TimeOfDay; // 23:37:57.4641984


نحوه محاسبه PersianDateTime.Now بر اساس فیلد استاتیک PersianDateTime.Mode است که مقدار پیش‌فرض آن PersianDateTimeMode.UtcOffset است.
PersianDateTime.Mode را می‌توان یکی از سه مقدار زیر قرار داد :
  • System : بر اساس تاریخ و زمان سیستم (Time Zone فعلی سیستم)
  • PersianTimeZoneInfo : بر اساس Time Zone تعیین شده در فیلد استاتیک PersianDateTime.PersianTimeZoneInfo (مستقل از Time Zone سیستم)
  • UtcOffset : بر اساس اختلاف ساعت با UTC، مشخص شده در فیلد استاتیک PersianDateTime.OffsetFromUtc (مستقل از Time Zone سیستم)

مقدار پیش‌فرض PersianDateTime.PersianTimeZoneInfo برابر Iran Standard Time Zone است. توجه داشته باشید که در این حالت از DaylightSavingTime تعیین شده در Time Zone استفاده خواهد شد که مثلا برای ایران با زمان واقعی آن اختلاف دارد و باید آنرا اصلاح کرد .

مقدار پیش‌فرض PersianDateTime.OffsetFromUtc برابر 3 ساعت و نیم است. در این حالت DaylightSavingTime با توجه به مقادیر سه فیلد استاتیک DaylightSavingTimeStart ،DaylightSavingTimeEnd و DaylightSavingTime تعیین می‌شود که به صورت پیش‌فرض برابر ساعت 24 اول فروردین، ساعت 24 سی‌ام شهریور و یک ساعت است.

تمام فیلدهای استاتیک ذکر شده به صورت public تعریف شده‌اند تا برنامه‌نویسان سایر کشورهای فارسی‌زبان بتوانند به دلخواه خود آنرا تغییر دهند.

نحوه کار با مقادیر رشته‌ای تاریخ شمسی هم اینگونه است :
PersianDateTime persianDate1 = PersianDateTime.Parse("1392/03/02");
PersianDateTime persianDate2 = PersianDateTime.Parse("1392/03/02", "23:32:56");

چند سازنده هم وجود دارد برای کسانی که تاریخ را به صورت int و ساعت را int یا short (بدون ثانیه) ذخیره می‌کنند :
PersianDateTime persianDate1 = new PersianDateTime(13920310);
PersianDateTime persianDate2 = new PersianDateTime(13920310, 233256);
PersianDateTime persianDate3 = new PersianDateTime(13920310, (short)2332);

تبدیل تاریخ میلادی به شمسی :
DateTime miladiDate = new DateTime(2013, 5, 31);
PersianDateTime persianDate = new PersianDateTime(miladiDate);

تبدیل تاریخ شمسی به میلادی :
PersianDateTime persianDate = PersianDateTime.Parse("1392/03/02");
DateTime miladiDate = persianDate.ToDateTime();


علاوه بر متد ToString معمولی، دو overload دیگر از این متد برای نمایش فرمت‌های مختلف PersianDateTime وجود دارد :

public string ToString(PersianDateTimeFormat format);

public string ToString(string format);
که اولی برای فرمت‌های معمول و دومی برای هر نوع فرمت دلخواه قابل استفاده است که چند نمونه آنرا در قسمت تعیین تاریخ و ساعت فعلی دیدید.

برخی از خصوصیات کلاس PersianDateTime :
  • Year : سال
  • Month : ماه
  • Day : روز
  • TimeOfDay : زمان سپری شده از ابتدای روز
  • TimeOfWeek :زمان سپری شده از ابتدای هفته
  • TimeOfMonth : زمان سپری شده از ابتدای ماه
  • TimeOfYear : زمان سپری شده از ابتدای سال
  • DaysInYear : تعداد روز سال
  • DaysInMonth : تعداد روز ماه
  • DayOfWeek : چندمین روز هفته
  • DayOfYear : چندمین روز سال
  • DayName : نام روز
  • MonthName : نام ماه
  • Date : تاریخ بدون زمان
  • FirstDayOfWeek : اولین روز هفته
  • LastDayOfWeek : آخرین روز هفته
  • FirstDayOfMonth : اولین روز ماه
  • LastDayOfMonth : آخرین روز ماه
  • FirstDayOfYear : اولین روز سال
  • LastDayOfYear : آخرین روز سال

برخی دیگر از متدهای کلاس PersianDateTime :

public PersianDateTime Add(TimeSpan value);
public PersianDateTime AddDays(double value);
public PersianDateTime AddMonths(int months);
public PersianDateTime AddYears(int value);
public PersianDateTime AddHours(double value);
public PersianDateTime AddMinutes(double value);
public PersianDateTime AddSeconds(double value);
همچنین تمام عملگرهای +، -، >، <، =>، =<، ==، و =! قابل استفاده هستند.
مطالب
تولید SiteMap استاندارد و ایجاد یک ActionResult اختصاصی برای Return کردن SiteMap تولید شده
یکی از item‌های مهم در بهینه سازی SEO یک وب‌سایت وجود یک SiteMap استاندارد متشکل از لینک‌های موجود در سایت هست که در وب‌سایت‌های داینامیک معمولا این لینک‌ها بر اساس داده‌های موجود در بانک اطلاعاتی ایجاد میشه. برای مثال مطالب، اخبار و ....
در اینجا بنده قبلا یک کلاس برای تولید SiteMap آماده کردم که در پروژه‌های خودم ازش استفاده میکنم. توسط این کلاس میتونید به صورت داینامیک SiteMap وب‌سایت مبتنی بر ASP.NET MVC خودتون رو ایجاد کنید.

برای آشنایی با ساختار یک SiteMap استاندارد میتونید به لینک رسمی روبرو مراجعه کنید : http://www.sitemaps.org/de/protocol.html 

بنده کلاس‌های زیر رو بر مبنای لینک مذکور در سایت رسمی SiteMaps تولید کردم.  بعد از تولید SiteMap نیاز دارید که اون رو مثلا به عنوان خروجی یک ActionResult بازگردونید. برای این کار هم یک کلاس با نام XmlResult  مشتق شده از ActionResult آماده سازی کردم که کلاس تولید شده SiteMap رو Serialize میکنه و به عنوان نتیجه‌ی یک Action باز می‌گردونه .
using System;
using System.Collections;
using System.Web.Mvc;
using System.Xml.Serialization;

namespace Neoox.Core.SeoTools
{
    [XmlRoot("urlset", Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")]
    public class Sitemap
    {
        private ArrayList map;

        public Sitemap()
        {
            map = new ArrayList();
        }

        [XmlElement("url")]
        public Location[] Locations
        {
            get
            {
                Location[] items = new Location[map.Count];
                map.CopyTo(items);
                return items;
            }
            set
            {
                if (value == null)
                    return;
                Location[] items = (Location[])value;
                map.Clear();
                foreach (Location item in items)
                    map.Add(item);
            }
        }

        public int Add(Location item)
        {
            return map.Add(item);
        }
    }

    public class Location
    {
        public enum eChangeFrequency
        {
            always,
            hourly,
            daily,
            weekly,
            monthly,
            yearly,
            never
        }

        [XmlElement("loc")]
        public string Url { get; set; }

        [XmlElement("changefreq")]
        public eChangeFrequency? ChangeFrequency { get; set; }
        public bool ShouldSerializeChangeFrequency() { return ChangeFrequency.HasValue; }

        [XmlElement("lastmod")]
        public DateTime? LastModified { get; set; }
        public bool ShouldSerializeLastModified() { return LastModified.HasValue; }

        [XmlElement("priority")]
        public double? Priority { get; set; }
        public bool ShouldSerializePriority() { return Priority.HasValue; }
    }

    public class XmlResult : ActionResult
    {
        private object objectToSerialize;

        public XmlResult(object objectToSerialize)
        {
            this.objectToSerialize = objectToSerialize;
        }

        public object ObjectToSerialize
        {
            get { return this.objectToSerialize; }
        }

        public override void ExecuteResult(ControllerContext context)
        {
            if (this.objectToSerialize != null)
            {
                context.HttpContext.Response.Clear();
                var xs = new System.Xml.Serialization.XmlSerializer(this.objectToSerialize.GetType());
                context.HttpContext.Response.ContentType = "text/xml";
                xs.Serialize(context.HttpContext.Response.Output, this.objectToSerialize);
            }
        }
    }
}
 و اما نحوه استفاده از این کلاس‌ها هم خیلی سادست. به مثال زیر توجه کنید ... فقط این نکته رو در نظر داشته باشید که item هایی که به sitemap اضافه میشه واکشی شده از بانک اطلاعاتی هست، در این مثال چون خواستم ساده توضیح داده بشه نحوه استفاده از این کلاس‌ها، این داده‌ها به صورت static در نظر گرفته شد ولی شما میتونید داده‌ها رو بر اساس ساختار بانک اطلاعاتی خودتون واکشی کرده و به SiteMap اضافه کنید تا یک SiteMap کاملا پویا و Dynamic داشته باشید...
        public ActionResult Sitemap()
        {
            Sitemap sm = new Sitemap();
            sm.Add(new Location()
            {
                Url = string.Format("http://www.TechnoDesign.ir/Articles/{0}/{1}", 1, "SEO-in-ASP.NET-MVC"),
                LastModified = DateTime.UtcNow,
                Priority = 0.5D
            });
            return new XmlResult(sm);
        }
مطالب
ایجاد فرم جستجوی پویا با استفاده از Expression ها
در مواردی نیاز است کاربر را جهت انتخاب فیلدهای مورد جستجو آزاد نگه داریم. برای نمونه جستجویی را در نظر بگیرید که کاربر قصد دارد: "دانش آموزانی که نام آنها برابر علی است و شماره دانش آموزی آنها از 100 کمتر است" را پیدا کند در شرایطی که فیلدهای نام و شماره دانش آموزی و عمل گر کوچک‌تر را خود کاربر به دلخواه برگزیرده.
روش‌های زیادی برای پیاده سازی این نوع جستجوها وجود دارد. در این مقاله سعی شده گام‌های ایجاد یک ساختار پایه برای این نوع فرم‌ها و یک ایجاد فرم نمونه بر پایه ساختار ایجاد شده را با استفاده از یکی از همین روش‌ها شرح دهیم.
اساس این روش تولید عبارت Linq بصورت پویا با توجه به انتخاب‌های کاربرمی باشد.
1-  برای شروع یک سلوشن خالی با نام DynamicSearch ایجاد می‌کنیم. سپس ساختار این سلوشن را بصورت زیر شکل می‌دهیم.


در این مثال پیاده سازی در قالب ساختار MVVM در نظر گرفته شده. ولی محدودتی از این نظر برای این روش قائل نیستیم.
2-  کار را از پروژه مدل آغاز می‌کنیم. جایی که ما برای سادگی کار، 3 کلاس بسیار ساده را به ترتیب زیر ایجاد می‌کنیم:
namespace DynamicSearch.Model
{
    public class Person
    {
        public Person(string name, string family, string fatherName)
        {
            Name = name;
            Family = family;
            FatherName = fatherName;
        }

        public string Name { get; set; }
        public string Family { get; set; }
        public string FatherName { get; set; }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DynamicSearch.Model
{
    public class Teacher : Person
    {
        public Teacher(int id, string name, string family, string fatherName)
            : base(name, family, fatherName)
        {
            ID = id;
        }

        public int ID { get; set; }

        public override string ToString()
        {
            return string.Format("Name: {0}, Family: {1}", Name, Family);
        }
    }
}

namespace DynamicSearch.Model
{
    public class Student : Person
    {
        public Student(int stdId, Teacher teacher, string name, string family, string fatherName)
            : base(name, family, fatherName)
        {
            StdID = stdId;
            Teacher = teacher;
        }

        public int StdID { get; set; }
        public Teacher Teacher { get; set; }
    }
}
3- در پروژه سرویس یک کلاس بصورت زیر ایجاد می‌کنیم:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicSearch.Model;

namespace DynamicSearch.Service
{
    public class StudentService
    {
        public IList<Student> GetStudents()
        {
            return new List<Student>
                {
                    new Student(1,new Teacher(1,"Ali","Rajabi","Reza"),"Mohammad","Hoeyni","Sadegh"),
                    new Student(2,new Teacher(2,"Hasan","Noori","Mohsen"),"Omid","Razavi","Ahmad"),
                };
        }
    }
}
4- تا اینجا تمامی داده‌ها صرفا برای نمونه بود. در این مرحله ساخت اساس جستجو گر پویا را شرح می‌دهیم.
جهت ساخت عبارت، نیاز به سه نوع جزء داریم:
-اتصال دهنده عبارات ( "و" ، "یا")
-عملوند (در اینجا فیلدی که قصد مقایسه با عبارت مورد جستجوی کاربر را داریم)
-عملگر ("<" ، ">" ، "=" ، ....)

برای ذخیره المان‌های انتخاب شده توسط کاربر، سه کلاس زیر را ایجاد می‌کنیم (همان سه جزء بالا):
using System;
using System.Linq.Expressions;

namespace DynamicSearch.ViewModel.Base
{
    public class AndOr
    {
        public AndOr(string name, string title,Func<Expression,Expression,Expression> func)
        {
            Title = title;
            Func = func;
            Name = name;
        }

        public string Title { get; set; }
        public Func<Expression, Expression, Expression> Func { get; set; }
        public string Name { get; set; }
    }
}

using System;

namespace DynamicSearch.ViewModel.Base
{
    public class Feild : IEquatable<Feild>
    {
        public Feild(string title, Type type, string name)
        {
            Title = title;
            Type = type;
            Name = name;
        }

        public Type Type { get; set; }
        public string Name { get; set; }
        public string Title { get; set; }
        public bool Equals(Feild other)
        {
            return other.Title == Title;
        }
    }
}

using System;
using System.Linq.Expressions;

namespace DynamicSearch.ViewModel.Base
{
    public class Operator
    {
        public enum TypesToApply
        {
            String,
            Numeric,
            Both
        }

        public Operator(string title, Func<Expression, Expression, Expression> func, TypesToApply typeToApply)
        {
            Title = title;
            Func = func;
            TypeToApply = typeToApply;
        }

        public string Title { get; set; }
        public Func<Expression, Expression, Expression> Func { get; set; }
        public TypesToApply TypeToApply { get; set; }
    }
}
توسط کلاس زیر یک سری اعمال متداول را پیاده سازی کرده ایم و پیاده سازی اضافات را بعهده کلاس‌های ارث برنده از این کلاس گذاشته ایم:

using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;

namespace DynamicSearch.ViewModel.Base
{
    public abstract class SearchFilterBase<T> : BaseViewModel
    {
        protected SearchFilterBase()
        {
            var containOp = new Operator("شامل باشد", (expression, expression1) => Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1), Operator.TypesToApply.String);
            var notContainOp = new Operator("شامل نباشد", (expression, expression1) =>
            {
                var contain = Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1);
                return Expression.Not(contain);
            }, Operator.TypesToApply.String);
            var equalOp = new Operator("=", Expression.Equal, Operator.TypesToApply.Both);
            var notEqualOp = new Operator("<>", Expression.NotEqual, Operator.TypesToApply.Both);
            var lessThanOp = new Operator("<", Expression.LessThan, Operator.TypesToApply.Numeric);
            var greaterThanOp = new Operator(">", Expression.GreaterThan, Operator.TypesToApply.Numeric);
            var lessThanOrEqual = new Operator("<=", Expression.LessThanOrEqual, Operator.TypesToApply.Numeric);
            var greaterThanOrEqual = new Operator(">=", Expression.GreaterThanOrEqual, Operator.TypesToApply.Numeric);

            Operators = new ObservableCollection<Operator>
                {
                      equalOp, 
                      notEqualOp,
                      containOp,
                      notContainOp,
                      lessThanOp,
                      greaterThanOp,
                      lessThanOrEqual,
                      greaterThanOrEqual,
                };


            SelectedAndOr = AndOrs.FirstOrDefault(a => a.Name == "Suppress");
            SelectedFeild = Feilds.FirstOrDefault();
            SelectedOperator = Operators.FirstOrDefault(a => a.Title == "=");
        }

        public abstract IQueryable<T> GetQuarable();

        public virtual ObservableCollection<AndOr> AndOrs
        {
            get
            {
                return new ObservableCollection<AndOr>
                    {
                        new AndOr("And","و", Expression.AndAlso), 
                        new AndOr("Or","یا",Expression.OrElse),
                        new AndOr("Suppress","نادیده",(expression, expression1) => expression),
                    };
            }
        }
        public virtual ObservableCollection<Operator> Operators
        {
            get { return _operators; }
            set { _operators = value; NotifyPropertyChanged("Operators"); }
        }
        public abstract ObservableCollection<Feild> Feilds { get; }

        public bool IsOtherFilters
        {
            get { return _isOtherFilters; }
            set { _isOtherFilters = value; }
        }
        public string SearchValue
        {
            get { return _searchValue; }
            set { _searchValue = value; NotifyPropertyChanged("SearchValue"); }
        }
        public AndOr SelectedAndOr
        {
            get { return _selectedAndOr; }
            set { _selectedAndOr = value; NotifyPropertyChanged("SelectedAndOr"); NotifyPropertyChanged("SelectedFeildHasSetted"); }
        }
        public Operator SelectedOperator
        {
            get { return _selectedOperator; }
            set { _selectedOperator = value; NotifyPropertyChanged("SelectedOperator"); }
        }
        public Feild SelectedFeild
        {
            get { return _selectedFeild; }
            set
            {
                Operators = value.Type == typeof(string) ? new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.String)) : new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.Numeric));
                if (SelectedOperator == null)
                {
                    SelectedOperator = Operators.FirstOrDefault(a => a.Title == "=");
                }

                NotifyPropertyChanged("SelectedOperator");
                NotifyPropertyChanged("SelectedFeild");
                _selectedFeild = value;
                NotifyPropertyChanged("SelectedFeildHasSetted");
            }
        }
        public bool SelectedFeildHasSetted
        {
            get
            {
                return SelectedFeild != null &&
                       (SelectedAndOr.Name != "Suppress" || !IsOtherFilters);
            }
        }

        private ObservableCollection<Operator> _operators;
        private Feild _selectedFeild;
        private Operator _selectedOperator;
        private AndOr _selectedAndOr;
        private string _searchValue;
        private bool _isOtherFilters = true;
    }
}
توضیحات: در این ویو مدل پایه سه لیست تعریف شده که برای دو تای آنها پیاده سازی پیش فرضی در همین کلاس دیده شده ولی برای لیست فیلدها پیاده سازی به کلاس ارث برنده واگذار شده است.

در گام بعد، یک کلاس کمکی برای سهولت ساخت عبارات ایجاد می‌کنیم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper;

namespace DynamicSearch.ViewModel.Base
{
  public static  class ExpressionExtensions
    {
        public static List<T> CreateQuery<T>(Expression whereCallExpression, IQueryable entities)
        {
            return entities.Provider.CreateQuery<T>(whereCallExpression).ToList();
        }

        public static MethodCallExpression CreateWhereCall<T>(Expression condition, ParameterExpression pe, IQueryable entities)
        {
            var whereCallExpression = Expression.Call(
                typeof(Queryable),
                "Where",
                new[] { entities.ElementType },
                entities.Expression,
                Expression.Lambda<Func<T, bool>>(condition, new[] { pe }));
            return whereCallExpression;
        }

        public static void CreateLeftAndRightExpression<T>(string propertyName, Type type, string searchValue, ParameterExpression pe, out Expression left, out Expression right)
        {
            var typeOfNullable = type;
            typeOfNullable = typeOfNullable.IsNullableType() ? typeOfNullable.GetTypeOfNullable() : typeOfNullable;
            left = null;

            var typeMethodInfos = typeOfNullable.GetMethods();
            var parseMethodInfo = typeMethodInfos.FirstOrDefault(a => a.Name == "Parse" && a.GetParameters().Count() == 1);

            var propertyInfos = typeof(T).GetProperties();
            if (propertyName.Contains("."))
            {
                left = CreateComplexTypeExpression(propertyName, propertyInfos, pe);
            }
            else
            {
                var propertyInfo = propertyInfos.FirstOrDefault(a => a.Name == propertyName);
                if (propertyInfo != null) left = Expression.Property(pe, propertyInfo);
            }

            if (left != null) left = Expression.Convert(left, typeOfNullable);

            if (parseMethodInfo != null)
            {
                var invoke = parseMethodInfo.Invoke(searchValue, new object[] { searchValue });
                right = Expression.Constant(invoke, typeOfNullable);
            }
            else
            {
                //type is string
                right = Expression.Constant(searchValue.ToLower());
                var methods = typeof(string).GetMethods();
                var firstOrDefault = methods.FirstOrDefault(a => a.Name == "ToLower" && !a.GetParameters().Any());
                if (firstOrDefault != null) left = Expression.Call(left, firstOrDefault);
            }
        }

        public static Expression CreateComplexTypeExpression(string searchFilter, IEnumerable<PropertyInfo> propertyInfos, Expression pe)
        {
            Expression ex = null;
            var infos = searchFilter.Split('.');
            var enumerable = propertyInfos.ToList();
            for (var index = 0; index < infos.Length - 1; index++)
            {
                var propertyInfo = infos[index];
                var nextPropertyInfo = infos[index + 1];
                if (propertyInfos == null) continue;
                var propertyInfo2 = enumerable.FirstOrDefault(a => a.Name == propertyInfo);
                if (propertyInfo2 == null) continue;
                var val = Expression.Property(pe, propertyInfo2);
                var propertyInfos3 = propertyInfo2.PropertyType.GetProperties();
                var propertyInfo3 = propertyInfos3.FirstOrDefault(a => a.Name == nextPropertyInfo);
                if (propertyInfo3 != null) ex = Expression.Property(val, propertyInfo3);
            }

            return ex;
        }

        public static Expression AddOperatorExpression(Func<Expression, Expression, Expression> func, Expression left, Expression right)
        {
            return func.Invoke(left, right);
        }

        public static Expression JoinExpressions(bool isFirst, Func<Expression, Expression, Expression> func, Expression expression, Expression ex)
        {
            if (!isFirst)
            {
                return func.Invoke(expression, ex);
            }

            expression = ex;
            return expression;
        }
    }
}
5- ایجاد کلاس فیلتر جهت معرفی فیلدها و معرفی منبع داده و ویو مدلی ارث برنده از کلاس‌های پایه ساختار، جهت ایجاد فرم نمونه:

using System.Collections.ObjectModel;
using System.Linq;
using DynamicSearch.Model;
using DynamicSearch.Service;
using DynamicSearch.ViewModel.Base;

namespace DynamicSearch.ViewModel
{
    public class StudentSearchFilter : SearchFilterBase<Student>
    {
        public override ObservableCollection<Feild> Feilds
        {
            get
            {
                return new ObservableCollection<Feild>
                    {
                        new Feild("نام دانش آموز",typeof(string),"Name"), 
                         new Feild("نام خانوادگی دانش آموز",typeof(string),"Family"),
                        new Feild("نام خانوادگی معلم",typeof(string),"Teacher.Name"),
                        new Feild("شماره دانش آموزی",typeof(int),"StdID"),
                    };
            }
        }

        public override IQueryable<Student> GetQuarable()
        {
            return new StudentService().GetStudents().AsQueryable();
        }
    }
}
6- ایجاد ویو نمونه:

در نهایت زمل فایل موجود در پروژه ویو:

<Window x:Class="DynamicSearch.View.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:viewModel="clr-namespace:DynamicSearch.ViewModel;assembly=DynamicSearch.ViewModel"
        xmlns:view="clr-namespace:DynamicSearch.View"
        mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Window.Resources>
        <viewModel:StudentSearchViewModel x:Key="StudentSearchViewModel" />
        <view:VisibilityConverter x:Key="VisibilityConverter" />
    </Window.Resources>
    <Grid   DataContext="{StaticResource StudentSearchViewModel}">
        <WrapPanel Orientation="Vertical">
            <DataGrid AutoGenerateColumns="False" Name="asd" CanUserAddRows="False" ItemsSource="{Binding BindFilter}">
                <DataGrid.Columns>
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <ComboBox MinWidth="100"  DisplayMemberPath="Title" ItemsSource="{Binding AndOrs}" Visibility="{Binding IsOtherFilters,Converter={StaticResource VisibilityConverter}}"
                                          SelectedItem="{Binding SelectedAndOr,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn >
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <ComboBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="100"   DisplayMemberPath="Title" ItemsSource="{Binding Feilds}" SelectedItem="{Binding SelectedFeild,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged }"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <ComboBox MinWidth="100"  DisplayMemberPath="Title" ItemsSource="{Binding Operators}" IsEnabled="{Binding SelectedFeildHasSetted}"
                                          SelectedItem="{Binding SelectedOperator,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <TextBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="200" Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                <!--<TextBox Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>-->
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>

            <Button Content="+" HorizontalAlignment="Left" Command="{Binding AddFilter}"/>
            <Button Content="Result" Command="{Binding ExecuteSearchFilter}"/>
            <DataGrid ItemsSource="{Binding Results}">
                
            </DataGrid>
        </WrapPanel>
    </Grid>
</Window>
در این مقاله، هدف معرفی روند ایجاد یک جستجو گر پویا با قابلیت استفاده مجدد بالا بود و عمدا از توضیح جزء به جزء کدها صرف نظر شده. علت این امر وجود منابع بسیار راجب ابزارهای بکار رفته در این مقاله و سادگی کدهای نوشته شده توسط اینجانب می‌باشد.


برخی منابع جهت آشنایی با Expression ها:
http://msdn.microsoft.com/en-us/library/bb882637.aspx
انتخاب پویای فیلد‌ها در LINQ 
http://www.persiadevelopers.com/articles/dynamiclinqquery.aspx


نکته: کدهای نوشته شده در این مقاله، نسخه‌های نخستین هستند و طبیعتا جا برای بهبود بسیار دارند. دوستان می‌توانند در این امر به بنده کمک کنند.


پیشنهادات جهت بهبود:
- جداسازی کدهای پیاده کننده منطق از ویو مدل‌ها جهت افزایش قابلیت نگهداری کد و سهولت استفاده در سایر ساختارها
- افزودن توضیحات به کد
- انتخاب نامگذاری‌های مناسب تر

DynamicSearch.zip
 
مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 3#
در ادامه مطالب قبل
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# 
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#

قبل از شروع توضیحات متد‌های کلاس Shape در ادامه پست‌های قبل در ^ و ^ ابتدا به تشریح یک تصویر می‌پردازیم.

نحوه ترسیم شی

خوب همانگونه که در تصویر بالا مشاده می‌نمایید، برای رسم یک شی چهار حالت متفاوت ممکن است پیش بیاید. (دقت کنید که ربع اول محور مختصات روی بوم گرافیکی قرار گرفته است، در واقع گوشه بالا و سمت چپ بوم گرافیکی نقطه (0 و 0) محور مختصات است و عرض بوم گرافیکی محور X‌ها و ارتفاع بوم گرافیکی محور Y‌ها را نشان می‌دهد)
  1. در این حالت StartPoint.X < EndPoint.X و StartPoint.Y < EndPoint.Y خواهد بود. (StartPoint نقطه ای است که ابتدا ماوس شروع به ترسیم می‌کند، و EndPoint زمانی است که ماوس رها شده و پایان ترسیم را مشخص می‌کند.)
  2. در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
  3. در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
  4. در این حالت StartPoint.X < EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.

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

using System;
using System.Drawing;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Helpers
    /// </summary>
    public static class Helpers
    {
        /// <summary>
        /// Draws the preview.
        /// </summary>
        /// <param name="g">The g.</param>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</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="backgroundBrush">The background brush.</param>
        /// <param name="shapeType">Type of the shape.</param>
        public static void DrawPreview(Graphics g, PointF startPoint, PointF endPoint, Color foreColor, byte thickness, bool isFill, Brush backgroundBrush, ShapeType shapeType)
        {
            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;
            }

            switch (shapeType)
            {
                case ShapeType.Ellipse:
                    if (isFill)
                        g.FillEllipse(backgroundBrush, x, y, width, height);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, width, height);
                    break;
                case ShapeType.Rectangle:
                    if (isFill)
                        g.FillRectangle(backgroundBrush, x, y, width, height);
                    //else
                    g.DrawRectangle(new Pen(foreColor, thickness), x, y, width, height);
                    break;
                case ShapeType.Circle:
                    float raduis = Math.Max(width, height);

                    if (isFill)
                        g.FillEllipse(backgroundBrush, x, y, raduis, raduis);
                    //else
                    g.DrawEllipse(new Pen(foreColor, thickness), x, y, raduis, raduis);
                    break;
                case ShapeType.Square:
                    float side = Math.Max(width, height);

                    if (isFill)
                        g.FillRectangle(backgroundBrush, x, y, side, side);
                    //else
                    g.DrawRectangle(new Pen(foreColor, thickness), x, y, side, side);
                    break;
                case ShapeType.Line:
                    g.DrawLine(new Pen(foreColor, thickness), startPoint, endPoint);
                    break;
                case ShapeType.Diamond:
                    var points = new PointF[4];
                    points[0] = new PointF(x + width / 2, y);
                    points[1] = new PointF(x + width, y + height / 2);
                    points[2] = new PointF(x + width / 2, y + height);
                    points[3] = new PointF(x, y + height / 2);
                    if (isFill)
                        g.FillPolygon(backgroundBrush, points);
                    //else
                    g.DrawPolygon(new Pen(foreColor, thickness), points);
                    break;
                case ShapeType.Triangle:
                    var tPoints = new PointF[3];
                    tPoints[0] = new PointF(x + width / 2, y);
                    tPoints[1] = new PointF(x + width, y + height);
                    tPoints[2] = new PointF(x, y + height);
                    if (isFill)
                        g.FillPolygon(backgroundBrush, tPoints);
                    //else
                    g.DrawPolygon(new Pen(foreColor, thickness), tPoints);
                    break;
            }
            if (shapeType != ShapeType.Line)
            {
                g.DrawString(String.Format("({0},{1})", x, y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x - 20, y - 25);
                g.DrawString(String.Format("({0},{1})", x + width, y + height), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x + width - 20, y + height + 5);
            }
            else
            {
                g.DrawString(String.Format("({0},{1})", startPoint.X, startPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), startPoint.X - 20, startPoint.Y - 25);
                g.DrawString(String.Format("({0},{1})", endPoint.X, endPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), endPoint.X - 20, endPoint.Y + 5);
            }

        }
    }
}
متد های این کلاس:
  • DrawPreview : این متد پیش نمایشی برای شی در زمان ترسیم، جابجایی و تغییر اندازه آماده می‌کند، پارامترهای آن عبارتند از : بوم گرافیکی، نقطه شروع، نقطه پایان و رنگ قلم ترسیم پیش نمایش شی، ضخامت خط، آیا شی توپر باشد؟، الگوی پر کردن پس زمینه شی ، و نوع شی ترسیمی می‌باشد.
در ادامه پست‌های قبل ادامه کد کلاس Shape را تشریح می‌کنیم.
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Net;

namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Shape (Base Class)
    /// </summary>
    public abstract partial class Shape
    {
#region Constructors (2) 

        /// <summary>
        /// Initializes a new instance of the <see cref="Shape" /> 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>
        protected Shape(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor)
        {
            CalulateLocationAndSize(startPoint, endPoint);
            Zindex = zIndex;
            ForeColor = foreColor;
            Thickness = thickness;
            IsFill = isFill;
            BackgroundColor = backgroundColor;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Shape" /> class.
        /// </summary>
        protected Shape() { }

#endregion Constructors 
        
#region Methods (10) 

// Public Methods (9) 

        /// <summary>
        /// Draws the specified g.
        /// </summary>
        /// <param name="g">The g.</param>
        public virtual void Draw(Graphics g)
        {
            if (!IsSelected) return;
            float diff = Thickness + 4;
            Color myColor = Color.DarkSeaGreen;
            g.DrawString(String.Format("({0},{1})", StartPoint.X, StartPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), StartPoint.X - 20, StartPoint.Y - 25);
            g.DrawString(String.Format("({0},{1})", EndPoint.X, EndPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), EndPoint.X - 20, EndPoint.Y + 5);
            if (ShapeType != ShapeType.Line)
            {
                g.DrawRectangle(new Pen(myColor), X, Y, Width, Height);

                //  1 2 3
                //  8   4 
                //  7 6 5   
                var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point2 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, StartPoint.Y - diff / 2);
                var point3 = new PointF(EndPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point4 = new PointF(EndPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2);
                var point5 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2);
                var point6 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, EndPoint.Y - diff / 2);
                var point7 = new PointF(StartPoint.X - diff / 2, EndPoint.Y - diff / 2);
                var point8 = new PointF(StartPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2);


                g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point3.X, point3.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point4.X, point4.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point5.X, point5.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point6.X, point6.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point7.X, point7.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point8.X, point8.Y, diff, diff);
            }
            else
            {
                var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2);
                var point2 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2);
                g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff);
                g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff);
            }
        }

        /// <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 virtual bool HasPointInSahpe(PointF point, byte tolerance = 5)
        {
            return point.X > (StartPoint.X - tolerance) && point.X < (EndPoint.X + tolerance) && point.Y > (StartPoint.Y - tolerance) && point.Y < (EndPoint.Y + tolerance);
        }

        /// <summary>
        /// Moves the specified location.
        /// </summary>
        /// <param name="location">The location.</param>
        /// <returns></returns>
        public virtual PointF Move(Point location)
        {
            StartPoint = new PointF(location.X, location.Y);
            EndPoint = new PointF(location.X + Width, location.Y + Height);
            return StartPoint;
        }

        /// <summary>
        /// Moves the specified dx.
        /// </summary>
        /// <param name="dx">The dx.</param>
        /// <param name="dy">The dy.</param>
        /// <returns></returns>
        public virtual PointF Move(int dx, int dy)
        {
            StartPoint = new PointF(StartPoint.X + dx, StartPoint.Y + dy);
            EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy);
            return StartPoint;
        }

        /// <summary>
        /// Resizes the specified dx.
        /// </summary>
        /// <param name="dx">The dx.</param>
        /// <param name="dy">The dy.</param>
        /// <returns></returns>
        public virtual SizeF Resize(int dx, int dy)
        {
            EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy);
            return new SizeF(Width, Height);
        }

        /// <summary>
        /// Resizes the specified start point.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="currentPoint">The current point.</param>
        public virtual void Resize(PointF startPoint, PointF currentPoint)
        {
            var dx = (int)(currentPoint.X - startPoint.X);
            var dy = (int)(currentPoint.Y - startPoint.Y);
            if (startPoint.X >= X - 5 && startPoint.X <= X + 5)
            {
                StartPoint = new PointF(currentPoint.X, StartPoint.Y);
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Height = Width;
                }
            }
            else if (startPoint.X >= EndPoint.X - 5 && startPoint.X <= EndPoint.X + 5)
            {
                Width += dx;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Height = Width;
                }
            }
            else if (startPoint.Y >= Y - 5 && startPoint.Y <= Y + 5)
            {
                Y = currentPoint.Y;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Width = Height;
                }
            }
            else if (startPoint.Y >= EndPoint.Y - 5 && startPoint.Y <= EndPoint.Y + 5)
            {
                Height += dy;
                if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square)
                {
                    Width = Height;
                }
            }
        }

        /// <summary>
        /// Sets the background brush as hatch.
        /// </summary>
        /// <param name="hatchStyle">The hatch style.</param>
        public virtual void SetBackgroundBrushAsHatch(HatchStyle hatchStyle)
        {
            var brush = new HatchBrush(hatchStyle, BackgroundColor);
            BackgroundBrush = brush;
        }

        /// <summary>
        /// Sets the background brush as linear gradient.
        /// </summary>
        public virtual void SetBackgroundBrushAsLinearGradient()
        {
            var brush = new LinearGradientBrush(StartPoint, EndPoint, ForeColor, BackgroundColor);
            BackgroundBrush = brush;
        }

        /// <summary>
        /// Sets the background brush as solid.
        /// </summary>
        public virtual void SetBackgroundBrushAsSolid()
        {
            var brush = new SolidBrush(BackgroundColor);
            BackgroundBrush = brush;
        }
// Private Methods (1) 

        /// <summary>
        /// Calulates the size of the location and.
        /// </summary>
        /// <param name="startPoint">The start point.</param>
        /// <param name="endPoint">The end point.</param>
        private void CalulateLocationAndSize(PointF startPoint, PointF endPoint)
        {
            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);
            EndPoint = new PointF(X + width, Y + height);
        }

#endregion Methods 
    }
}

حال به تشریح سازنده کلاس می‌پردازیم:
  • Shape: پارامترهای این سازنده به ترتیب عبارتند از نقطه شروع، نقطه پایان، عمق شی، رنگ قلم، ضخامت خط، آیا شی توپر باشد؟، و رنگ پر کردن شی، در این سازنده ابتدا توسط متدی به نام CalulateLocationAndSize(startPoint, endPoint); b نقاط ابتدا و انتهای شی مورد نظر تنظیم می‌شود، در متد مذکور بررسی می‌شود در صورتی که نقاط شروع و پایان یکی از حالت‌های 1 ، 2، 3، 4 از تصویر ابتدا پست باشد همگی تبدیل به حالت 1 خواهد شد.

سپس به تشریح متدهای کلاس Shape می‌پردازیم:

  • Draw: این متد دارای یک پارامتر ورودی است که بوم گرافیکی مورد نظر می‌باشد، در واقع شی مورد نظر خود را بروی این بوم گرافیکی ترسیم می‌کند. در کلاس پایه کار این متد زیاد پیچیده نیست، در صورتی که شی در حالت انتخاب باشد (IsSelected = true) بروی شی مورد نظر 8 مربع کوچک ترسیم می‌شود و اگر شی مورد نظر خط باشد دو مربع کوچک در طرفین خط رسم می‌شود که نشان دهنده انتخاب شدن شی مورد نظر است. این متد به صورت virtual تعریف شده است یعنی کلاس هایی که از Shape ارث میبرند می‌توانند این متد را برای خود از نو بازنویسی کرده (override کنند) و تغییر رفتار دهند.
  • HasPointInSahpe : این متد نیز به صورت virtual تعریف شده است دارای خروجی بولین می‌باشد. پارامتر‌های این متد عبارتند از یک نقطه و یک عدد که نشان دهنده تلرانش نقطه بر حسب پیکسل می‌باشد. کار این متد این است که یک نقطه را گرفته و بررسی می‌کند که آیا نقطه مورد نظر با تلرانس وارد شده آیا در داخل شی واقع شده است یا خیر (مثلا وجود نقطه در مستطیل یا وجود نقطه در دایره فرمول‌های متفاوتی دارند که در اینجا پیش فرض برای تمامی اشیا حالت مستطیل در نظر گرفته شده که می‌توانید آنها را بازنویسی (override) کنید).
  • Move: این متد به عنوان پارامتر یک نقطه را گرفته و شی مورد نظر را به آن نقطه منتقل می‌کند در واقع نقطه شروع و پایان ترسیم شی را تغییر می‌دهد.
  • Move: این متد نیز برای جابجایی شی به کار می‌رود، این متد دارای پارامترهای جابجابی در راستای محور Xها , جابجایی در راستای محور Yها؛ و شی مورد نظر را به آن نقطه منتقل می‌کند در واقع نقطه شروع و پایان ترسیم شی را با توجه به پارامترهای ورودی تغییر می‌دهد. 
  • Resize: این متد نیز برای تغییر اندازه شی به کار می‌رود، این متد دارای پارامترهای تغییر اندازه در راستای محور Xها , تغییر اندازه در راستای محور Yها می‌باشد و نقطه پایان شی مورد نظر را تغییر می‌دهد اما نقطه شروع تغییری نمی‌کند.
  • Resize: این متد نیز برای تغییر اندازه شی به کار می‌رود، در زمان تغییر اندازه شی با ماوس ابتدا یک نقطه شروع وجود دارد که ماوس در آن نقطه کلیک شده و شروع به درگ کردن شی جهت تغییر اندازه می‌کند (پارامتر اول این متد نقطه شروع درگ کردن جهت تغییر اندازه را مشخص می‌کند startPoint)، سپس در یک نقطه ای درگ کردن تمام می‌شود در این نقطه باید شی تغییر اندازه پیدا کرده و ترسیم شود ( پارامتر دوم این متد نقطه مذکور می‌باشد currentLocation). سپس با توجه با این دو نقطه بررسی می‌شود که تغییر اندازه در کدام جهت صورت گرفته است و اعداد جهت تغییرات نقاط شروع و پایان شی مورد نظر محاسبه می‌شوند. (مثلا تغییر اندازه در مستطیل از ضلع بالا به طرفین، یا از ضلع سمت راست به طرفین و ....). البته برای مربع و دایره باید کاری کنیم که طول و عرض تغییر اندازه یکسان باشد.
  • CalulateLocationAndSize: این متد که در سازنده کلاس استفاده شده در واقع دو نقطه شروع و پایان را گرفته و با توجه به تصویر ابتدای پست حالت‌های 1 و 2 و3  و 4 را به حالت 1 تبدیل کرده و StartPoint و EndPoint را اصلاح می‌کند.
  • SetBackgroundBrushAsHatch: این متد یک الگوی Brush گرفته و با توجه به رنگ پس زمینه شی خصوصیت BackgroundBrush را مقداردهی می‌کند.
  • SetBackgroundBrushAsLinearGradient: این متد با توجه به خصوصیت ForeColor و BackgroundColor یک Gradiant Brush ساخته و آن را به خصوصیت
    BackgroundBrush نسبت می‌کند. 
  • SetBackgroundBrushAsSolid: یک الگوی پر کردن توپر برای شی مورد نظر با توجه به خصوصیت BackgroundColor شی ایجاد کرده و آن را به خصوصیت BackgroundBrush شی نسبت می‌دهد.

تذکر: متد‌های Move، Resize و HasPointInShape به صورت virtual تعریف شده تا کلاس‌های مشتق شده در صورت نیاز خود کد رفتار مورد نظر خود را override کرده یا از همین رفتار استفاده نمایند.

خوشحال می‌شم در صورتی که در Refactoring کد نوشته شده با من همکاری کنید.

در پست‌های آینده به بررسی و پیاده سازی دیگر کلاس‌ها خواهیم پرداخت.

مطالب
MongoDb در سی شارپ (بخش دهم)

ابتدا بسته زیر را از طریق  nuget نصب نمایید:

dotnet add package MongoDB.Driver


سپس مدل‌های زیر را ایجاد نمایید:

public class BaseModel
{
    public BaseModel()
    {
        CreationDate=DateTime.Now;
    }
    public string Id { get; set; }
    public DateTime CreationDate { get; set; }
    public bool IsRemoved { get; set; }
    public DateTime? ModificationDate { get; set; }

}


 این مدل شامل یک کلاس پایه برای id,CreationDate,ModificationDate,IsRemoved میباشد که بسیار شبیه مدل‌هایی است که عموما در EntityFramework تعریف می‌کنیم.

برای اینکه فیلد Id به صورت objectId ایجاد شود ولی به صورت رشته‌ای استفاده شود ابتدا ویژگی BsonId را در بالای آن تعریف کرده تا به عنوان شناسه یکتا سند شناخته شود و سپس با استفاده از ویژگی BsonRepresentation  اعلام میکنیم که کار تبدیل به رشته و بلعکس آن به صورت خودکار در پشت صحنه صورت بگیرد:

public class BaseModel
    {
        [BsonId]
        [BsonRepresentation((BsonType.ObjectId))]
        public string Id { get; set; }
    }

 البته این حالت برای زمانی مناسب است که ما در استفاده از ویژگی‌ها محدودیتی نداشته باشیم؛ ولی در بسیاری از نرم افزارها که از معماری‌های چند لایه مانند لایه پیازی استفاده میشود استفاده از این خصوصیت‌ها یعنی اعمال کارکرد کتابخانه بالاتر بر روی لایه‌های زیرین که هسته نرم افزار شناخته میشوند که صحیح نبوده و باید توسط لایه‌های بالاتر این تغییرات اعمال شوند که میتواند از طریق کلاس این کار را انجام دهید. به ازای هر مدل که نیاز به تغییرات دارد، یک حالت جدید تعریف شده و در ابتدای برنامه در فایل Program.cs یا قبل از دات نت 6 در Startup.cs صدا زده می‌شوند.

BsonClassMap.RegisterClassMap<BaseModel>(map =>
{
    map.SetIdMember(map.GetMemberMap(x=>x.Id));
    map.GetMemberMap(x => x.Id)
        .SetSerializer(new StringSerializer(BsonType.ObjectId));
});


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

public class Employee : BaseModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
//=================
BsonClassMap.RegisterClassMap<Employee >(map =>
{
    map.SetIdMember(map.GetMemberMap(x=>x.Id));
    map.GetMemberMap(x => x.Id)
        .SetSerializer(new StringSerializer(BsonType.ObjectId));
});


روش استفاده از مونگو در asp.net core  به صورت زیر بسیار متداول میباشد که در قسمت‌های پیشین هم در این مورد نوشته بودیم:

MongoDbContext

  public interface IMongoDbContext
    {
        IMongoCollection<TEntity> GetCollection<TEntity>();
    }

  public class MongoDbContext : IMongoDbContext
    {

        private readonly IMongoClient _client;
        private readonly IMongoDatabase _database;

        public MongoDbContext(string databaseName,string connectionString)
        {
            var settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString));
            _client = new MongoClient(settings);
            _database = _client.GetDatabase(databaseName);
        }

        public IMongoCollection<TEntity> GetCollection<TEntity>()
        {
            return _database.GetCollection<TEntity>(typeof(TEntity).Name.ToLower() + "s");
        }
    }

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

public static class MongoDbContextService
{
    public static void AddMongoDbContext(this IServiceCollection services,string databaseName,string connectionString)
    {
        services.AddSingleton<IMongoDbContext>(serviceProvider => new MongoDbContext(databaseName, connectionString));
    }
}

//===============
Program.cs

builder.Services.AddMongoDbContext("bookstore", "mongodb://localhost:27017");


پیاده سازی SoftDelete در مونگو

در مونگو چیزی تحت عنوان Global Query Filter نداریم که تمام کوئری هایی که به سمت دیتابیس ارسال میشوند، توسط کانتکس اطلاح شوند؛ بدین جهت برای پیاده سازی این خصوصیت میتوان اینترفیسی با نام <IRepository<T را به شکل زیر طراحی نماییم:

public interface IRepository<T> where T : BaseModel
{

    IMongoCollection<T> GetCollection();
    IMongoQueryable<T> GetFilteredCollection();
}

public class Repository<T> : IRepository<T> where T:BaseModel
{
    private IMongoDbContext _mongoDbContext;

    public Repository(IMongoDbContext mongoDbContext)
    {
        _mongoDbContext = mongoDbContext;
    }

    public IMongoCollection<T> GetCollection()
    {
        return _mongoDbContext.GetCollection<T>();
    }
    
    public IMongoQueryable<T> GetFilteredCollection()
    {
        var query= _mongoDbContext.GetCollection<T>().AsQueryable();
        
        //================= Global Query Filters ====================
        
        //Filter 1
        query=query.Where(x => x.RemovedAt.HasValue == false);
        
        //==============================================================
        
        return query;
    }
}

این کلاس یا اینترفیس شامل دو متد هستند که کلاس جنریک آنها باید از BaseModel ارث بری کرده باشد و اولین متد، تنها یک کالکشن بدون هیچگونه فیلتری است که میتواند نقش متد IgnoreQueryFilters  را بازی کند و دیگری GetFilteredCollection است که در این متد ابتدا کالکشنی دریافت شده و سپس آن را به حالت کوئری تغییر داده و فیلترهای مورد نظر، مانند حذف منطقی را پیاده سازی میکنیم:

public interface IRepository<T> where T : BaseModel
{

    IMongoCollection<T> GetCollection();
    IMongoQueryable<T> GetFilteredCollection();
}

public class Repository<T> : IRepository<T> where T:BaseModel
{
    private IMongoDbContext _mongoDbContext;

    public Repository(IMongoDbContext mongoDbContext)
    {
        _mongoDbContext = mongoDbContext;
    }

    public IMongoCollection<T> GetCollection()
    {
        return _mongoDbContext.GetCollection<T>();
    }
    
    public IMongoQueryable<T> GetFilteredCollection()
    {
        var query= _mongoDbContext.GetCollection<T>().AsQueryable();
        
        //================= Global Query Filters ====================
        
        //Filter 1
        query=query.Where(x => x.RemovedAt.HasValue == false);
        
        //==============================================================
        
        return query;
    }
}


اصلاح تاریخ ویرایش در مدل

در EF به لطف dbset و همچنین ChangeTracking امکان شناسایی حالت‌ها وجود دارد و میتوانید در متدی مانند saveChanges مقدار تاریخ ویرایش را تنظیم نمود. برای مدل‌های منگو چنین چیزی وجود ندارد و به همین دلیل چند روش زیر پیشنهاد میگردد:

یک. استفاده از اینترفیس INotifyPropertyChanged یا جهت حذف کدهای تکراری نیز از الگوی AOP بهره بگیرید.

دو. استفاده از یک <Repository<T همانند بالا که شامل متدهای داخلی Update و Delete هستند که در آنجا میتوانید این مقادیر را به صورت مستقیم تغییر دهید.