معرفی الگوی Repository
روش متداول کار با فناوریهای مختلف دسترسی به دادهها عموما بدین شکل است:
الف) یافتن رشته اتصالی رمزنگاری شده به دیتابیس از یک فایل کانفیگ (در یک برنامه اصولی البته!)
ب) باز کردن یک اتصال به دیتابیس
ج) ایجاد اشیاء Command برای انجام عملیات مورد نظر
د) اجرا و فراخوانی اشیاء مراحل قبل
ه) بستن اتصال به دیتابیس و آزاد سازی اشیاء
اگر در برنامههای یک تازه کار به هر محلی از برنامه او دقت کنید این 5 مرحله را میتوانید مشاهده کنید. همه جا! قسمت ثبت، قسمت جستجو، قسمت نمایش و ...
مشکلات این روش:
1- حجم کارهای تکراری انجام شده بالا است. اگر قسمتی از فناوری دسترسی به دادهها را به اشتباه درک کرده باشد، پس از مطالعه بیشتر و مشخص شدن نحوهی رفع مشکل، قسمت عمدهای از برنامه را باید اصلاح کند (زیرا کدهای تکراری همه جای آن پراکندهاند).
2- برنامه نویس هر بار باید این مراحل را به درستی انجام دهد. اگر در یک برنامه بزرگ تنها قسمت آخر در یکی از مراحل کاری فراموش شود دیر یا زود برنامه تحت فشار کاری بالا از کار خواهد افتاد (و متاسفانه این مساله بسیار شایع است).
3- برنامه منحصرا برای یک نوع دیتابیس خاص تهیه خواهد شد و تغییر این رویه جهت استفاده از دیتابیسی دیگر (مثلا کوچ برنامه از اکسس به اس کیوال سرور)، نیازمند بازنویسی کل برنامه میباشد.
و ...
همین برنامه نویس پس از مدتی کار به این نتیجه میرسد که باید برای اینکارهای متداول، یک لایه و کلاس دسترسی به دادهها را تشکیل دهد. اکنون هر قسمتی از برنامه برای کار با دیتابیس باید با این کلاس مرکزی که انجام کارهای متداول با دیتابیس را خلاصه میکند، کار کند. به این صورت کد نویسی یک نواختی با حذف کدهای تکراری از سطح برنامه و همچنین بدون فراموش شدن قسمت مهمی از مراحل کاری، حاصل میگردد. در اینجا اگر روزی قرار شد از یک دیتابیس دیگر استفاده شود فقط کافی است یک کلاس برنامه تغییر کند و نیازی به بازنویسی کل برنامه نخواهد بود.
این روزها تشکیل این لایه دسترسی به دادهها (data access layer یا DAL) نیز مزموم است! و دلایل آن در مباحث چرا به یک ORM نیازمندیم برشمرده شده است. جهت کار با ORM ها نیز نیازمند یک لایه دیگر میباشیم تا یک سری اعمال متداول با آنهارا کپسوله کرده و از حجم کارهای تکراری خود بکاهیم. برای این منظور قبل از اینکه دست به اختراع بزنیم، بهتر است به الگوهای طراحی برنامه نویسی شیء گرا رجوع کرد و از رهنمودهای آن استفاده نمود.
الگوی Repository یکی از الگوهای برنامه نویسی با مقیاس سازمانی است. با کمک این الگو لایهای بر روی لایه نگاشت اشیاء برنامه به دیتابیس تشکیل شده و عملا برنامه را مستقل از نوع ORM مورد استفاه میکند. به این صورت هم از تشکیل یک سری کدهای تکراری در سطح برنامه جلوگیری شده و هم از وابستگی بین مدل برنامه و لایه دسترسی به دادهها (که در اینجا همان NHibernate میباشد) جلوگیری میشود. الگوی Repository (مخزن)، کار ثبت، حذف، جستجو و به روز رسانی دادهها را با ترجمه آنها به روشهای بومی مورد استفاده توسط ORM مورد نظر، کپسوله میکند. به این شکل شما میتوانید یک الگوی مخزن عمومی را برای کارهای خود تهیه کرده و به سادگی از یک ORM به ORM دیگر کوچ کنید؛ زیرا کدهای برنامه شما به هیچ ORM خاصی گره نخورده و این عملیات بومی کار با ORM توسط لایهای که توسط الگوی مخزن تشکیل شده، صورت گرفته است.
طراحی کلاس مخزن باید شرایط زیر را برآورده سازد:
الف) باید یک طراحی عمومی داشته باشد و بتواند در پروژههای متعددی مورد استفاده مجدد قرار گیرد.
ب) باید با سیستمی از نوع اول طراحی و کد نویسی و بعد کار با دیتابیس، سازگاری داشته باشد.
ج) باید امکان انجام آزمایشات واحد را سهولت بخشد.
د) باید وابستگی کلاسهای دومین برنامه را به زیر ساخت ORM مورد استفاده قطع کند (اگر سال بعد به این نتیجه رسیدید که ORM ایی به نام XYZ برای کار شما بهتر است، فقط پیاده سازی این کلاس باید تغییر کند و نه کل برنامه).
ه) باید استفاده از کوئریهایی از نوع strongly typed را ترویج کند (مثل کوئریهایی از نوع LINQ).
بررسی مدل برنامه
مدل این قسمت (برنامه NHSample4 از نوع کنسول با همان ارجاعات متداول ذکر شده در قسمتهای قبل)، از نوع many-to-many میباشد. در اینجا یک واحد درسی توسط چندین دانشجو میتواند اخذ شود یا یک دانشجو میتواند چندین واحد درسی را اخذ نماید که برای نمونه کلاس دیاگرام و کلاسهای متشکل آن به شکل زیر خواهند بود:
using System.Collections.Generic;
namespace NHSample4.Domain
{
public class Course
{
public virtual int Id { get; set; }
public virtual string Teacher { get; set; }
public virtual IList<Student> Students { get; set; }
public Course()
{
Students = new List<Student>();
}
}
}
using System.Collections.Generic;
namespace NHSample4.Domain
{
public class Student
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Course> Courses { get; set; }
public Student()
{
Courses = new List<Course>();
}
}
}
کلاس کانفیگ برنامه جهت ایجاد نگاشتها و سپس ساخت دیتابیس متناظر
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;
namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample4.Domain.Course).Assembly))
.ExportTo(System.Environment.CurrentDirectory)
);
}
public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
الف) با توجه به اینکه برنامه از نوع ویندوزی است، برای مدیریت صحیح کانکشن استرینگ، فایل App.Config را به برنامه افروده و محتویات آنرا به شکل زیر تنظیم میکنیم (تا کلید DbConnectionString توسط متد GetConfig مورد استفاده قرارگیرد ):
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>
</configuration>
ب) در NHibernate سنتی (!) کار ساخت نگاشتها توسط یک سری فایل xml صورت میگیرد که با معرفی فریم ورک Fluent NHibernate و استفاده از قابلیتهای Auto Mapping آن، اینکار با سهولت و دقت هر چه تمامتر قابل انجام است که توضیحات نحوهی انجام آنرا در قسمتهای قبل مطالعه فرمودید. اگر نیاز بود تا این فایلهای XML نیز جهت بررسی شخصی ایجاد شوند، تنها کافی است از متد ExportTo آن همانگونه که در متد GetConfig استفاده شده، کمک گرفته شود. به این صورت پس از ایجاد خودکار نگاشتها، فایلهای XML متناظر نیز در مسیری که به عنوان آرگومان متد ExportTo مشخص گردیده است، تولید خواهند شد (دو فایل NHSample4.Domain.Course.hbm.xml و NHSample4.Domain.Student.hbm.xml را در پوشهای که محل اجرای برنامه است خواهید یافت).
با فراخوانی متد CreateDb این کلاس، پس از ساخت خودکار نگاشتها، database schema متناظر، در دیتابیسی که توسط کانکشن استرینگ برنامه مشخص شده، ایجاد خواهد شد که دیتابیس دیاگرام آنرا در شکل ذیل مشاهده مینمائید (جداول دانشجویان و واحدها هر کدام به صورت موجودیتی مستقل ایجاد شده که ارجاعات آنها در جدولی سوم نگهداری میشود).
پیاده سازی الگوی مخزن
اینترفیس عمومی الگوی مخزن به شکل زیر میتواند باشد:
using System;
using System.Linq;
using System.Linq.Expressions;
namespace NHSample4.NHRepository
{
//Repository Interface
public interface IRepository<T>
{
T Get(object key);
T Save(T entity);
T Update(T entity);
void Delete(T entity);
IQueryable<T> Find();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
}
سپس پیاده سازی آن با توجه به کلاس SingletonCore ایی که در قسمت قبل تهیه کردیم (جهت مدیریت صحیح سشن فکتوری)، به صورت زیر خواهد بود.
این کلاس کار آغاز و پایان تراکنشها را نیز مدیریت کرده و جهت سهولت کار اینترفیس IDisposable را نیز پیاده سازی میکند :
using System;
using System.Linq;
using NHSessionManager;
using NHibernate;
using NHibernate.Linq;
namespace NHSample4.NHRepository
{
public class Repository<T> : IRepository<T>, IDisposable
{
private ISession _session;
private bool _disposed = false;
public Repository()
{
_session = SingletonCore.SessionFactory.OpenSession();
BeginTransaction();
}
~Repository()
{
Dispose(false);
}
public T Get(object key)
{
if (!isSessionSafe) return default(T);
return _session.Get<T>(key);
}
public T Save(T entity)
{
if (!isSessionSafe) return default(T);
_session.Save(entity);
return entity;
}
public T Update(T entity)
{
if (!isSessionSafe) return default(T);
_session.Update(entity);
return entity;
}
public void Delete(T entity)
{
if (!isSessionSafe) return;
_session.Delete(entity);
}
public IQueryable<T> Find()
{
if (!isSessionSafe) return null;
return _session.Linq<T>();
}
public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
if (!isSessionSafe) return null;
return Find().Where(predicate);
}
void Commit()
{
if (!isSessionSafe) return;
if (_session.Transaction != null &&
_session.Transaction.IsActive &&
!_session.Transaction.WasCommitted &&
!_session.Transaction.WasRolledBack)
{
_session.Transaction.Commit();
}
else
{
_session.Flush();
}
}
void Rollback()
{
if (!isSessionSafe) return;
if (_session.Transaction != null && _session.Transaction.IsActive)
{
_session.Transaction.Rollback();
}
}
private bool isSessionSafe
{
get
{
return _session != null && _session.IsOpen;
}
}
void BeginTransaction()
{
if (!isSessionSafe) return;
_session.BeginTransaction();
}
public void Dispose()
{
Dispose(true);
// tell the GC that the Finalize process no longer needs to be run for this object.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposeManagedResources)
{
if (_disposed) return;
if (!disposeManagedResources) return;
if (!isSessionSafe) return;
try
{
Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Rollback();
}
finally
{
if (isSessionSafe)
{
_session.Close();
_session.Dispose();
}
}
_disposed = true;
}
}
}
using System;
using System.Collections.Generic;
using NHSample4.Domain;
using NHSample4.NHRepository;
namespace NHSample4
{
class Program
{
static void Main(string[] args)
{
//ایجاد دیتابیس در صورت نیاز
//NHSessionManager.Config.CreateDb();
//ابتدا یک دانشجو را اضافه میکنیم
Student student = null;
using (var studentRepo = new Repository<Student>())
{
student = studentRepo.Save(new Student() { Name = "Vahid" });
}
//سپس یک واحد را اضافه میکنیم
using (var courseRepo = new Repository<Course>())
{
var course = courseRepo.Save(new Course() { Teacher = "Shams" });
}
//اکنون یک واحد را به دانشجو انتساب میدهیم
using (var courseRepo = new Repository<Course>())
{
courseRepo.Save(new Course() { Students = new List<Student>() { student } });
}
//سپس شماره دروس استادی خاص را نمایش میدهیم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");
foreach (var course in query)
Console.WriteLine(course.Id);
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
همانطور که ملاحظه میکنید در این سطح دیگر برنامه هیچ درکی از ORM مورد استفاده ندارد و پیاده سازی نحوهی تعامل با NHibernate در پس کلاس مخزن مخفی شده است. کار آغاز و پایان تراکنشها به صورت خودکار مدیریت گردیده و همچنین آزاد سازی منابع را نیز توسط اینترفیس IDisposable مدیریت میکند. به این صورت امکان فراموش شدن یک سری از اعمال متداول به حداقل رسیده، میزان کدهای تکراری برنامه کم شده و همچنین هر زمانیکه نیاز بود، صرفا با تغییر پیاده سازی کلاس مخزن میتوان به ORM دیگری کوچ کرد؛ بدون اینکه نیازی به بازنویسی کل برنامه وجود داشته باشد.
دریافت سورس برنامه قسمت هشتم
ادامه دارد ...
ASP.NET MVC #11
در هنگام create مقدار ModelState.IsValid ، برابر با false است ایا راهکار این لینک ، راهکار مناسبی است؟
ModelState.Remove("Id");
الف) مقیاس پذیری سمت سرور
در اعمال سمت سرور متداول، تردهای متعددی جهت پردازش درخواستهای کلاینتها تدارک دیده میشوند. هر زمانیکه یکی از این تردها، یک عملیات blocking را انجام میدهد (مانند دسترسی به شبکه یا اعمال I/O)، ترد مرتبط با آن تا پایان کار این عملیات معطل خواهد شد. با بالا رفتن تعداد کاربران یک برنامه و در نتیجه بیشتر شدن تعداد درخواستهایی که سرور باید پردازش کند، تعداد تردهای معطل مانده نیز به همین ترتیب بیشتر خواهند شد. مشکل اصلی اینجا است که نمونه سازی تردها بسیار هزینه بر است (با اختصاص 1MB of virtual memory space) و منابع سرور محدود. با زیاد شدن تعداد تردهای معطل اعمال I/O یا شبکه، سرور مجبور خواهد شد بجای استفاده مجدد از تردهای موجود، تردهای جدیدی را ایجاد کند. همین مساله سبب بالا رفتن بیش از حد مصرف منابع و حافظه برنامه میگردد. یکی از روشهای رفع این مشکل بدون نیاز به بهبودهای سخت افزاری، تبدیل اعمال blocking نامبرده شده به نمونههای non-blocking است. به این ترتیب ترد پردازش کنندهی این اعمال Async بلافاصله آزاد شده و سرور میتواند از آن جهت پردازش درخواست دیگری استفاده کند؛ بجای اینکه ترد جدیدی را وهله سازی نماید.
ب) بالا بردن پاسخ دهی کلاینتها
کلاینتها نیز اگر مدام درخواستهای blocking را به سرور جهت دریافت پاسخی ارسال کنند، به زودی به یک رابط کاربری غیرپاسخگو خواهند رسید. برای رفع این مشکل نیز میتوان از توانمندیهای Async دات نت 4.5 جهت آزاد سازی ترد اصلی برنامه یا همان ترد UI استفاده کرد.
و ... تمام اینها یک شرط را دارند. نیاز است یک چنین API خاصی که اعمال Async واقعی را پشتیبانی میکنند، فراهم شده باشد. بنابراین صرفا وجود متد Task.Run، به معنای اجرای واقعی Async یک متد خاص نیست. برای این منظور ADO.NET 4.5 به همراه متدهای Async ویژه کار با بانکهای اطلاعاتی است و پس از آن Entity framework 6 از این زیر ساخت استفاده کردهاست که در ادامه جزئیات آنرا بررسی خواهیم کرد.
پیشنیازها
برای کار با امکانات جدید Async موجود در EF 6 نیاز است از VS 2012 به بعد که به همراه کامپایلری است که واژههای کلیدی async و await را پشتیبانی میکند و همچنین دات نت 4.5 استفاده کرد. چون ADO.NET 4.5 اعمال async واقعی را پشتیبانی میکند، دات نت 4 در اینجا قابل استفاده نخواهد بود.
متدهای الحاقی جدید Async در EF 6.x
جهت متدهای الحاقی متداول EF مانند ToList، Max، Min و غیره، نمونههای Async آنها نیز اضافه شدهاند:
QueryableExtensions: AllAsync AnyAsync AverageAsync ContainsAsync CountAsync FirstAsync FirstOrDefaultAsync ForEachAsync LoadAsync LongCountAsync MaxAsync MinAsync SingleAsync SingleOrDefaultAsync SumAsync ToArrayAsync ToDictionaryAsync ToListAsync DbSet: FindAsync DbContext: SaveChangesAsync Database: ExecuteSqlCommandAsync
چند مثال
فرض کنید، مدلهای برنامه، رابطهی one-to-many ذیل را بین یک کاربر و مقالات او دارند:
public class User { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } } public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } [ForeignKey("UserId")] public virtual User User { get; set; } public int UserId { get; set; } }
public class MyContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<BlogPost> BlogPosts { get; set; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } }
private async Task<User> addUserAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user = context.Users.Add(new User { Name = "Vahid" }); context.BlogPosts.Add(new BlogPost { Content = "Test", Title = "Test", User = user }); await context.SaveChangesAsync(cancellationToken); return user; } }
چند نکته جهت یادآوری مباحث Async
- به امضای متد واژهی کلیدی async اضافه شدهاست، زیرا در بدنهی آن از کلمهی کلیدی await استفاده کردهایم (لازم و ملزوم هستند).
- به انتهای نام متد، کلمهی Async اضافه شدهاست. این مورد ضروری نیست؛ اما به یک استاندارد و قرارداد تبدیل شدهاست.
- مدل Async دات نت 4.5 مبتنی بر Taskها است. به همین جهت اینبار خروجیهای توابع نیاز است از نوع Task باشند و آرگومان جنریک آنها، بیانگر نوع مقداری که باز میگردانند.
- تمام متدهای الحاقی جدیدی که نامبرده شدند، دارای پارامتر اختیاری لغو عملیات نیز هستند. این مورد را با مقدار دهی cancellationToken در کدهای فوق ملاحظه میکنید.
نمونهای از نحوهی مقدار دهی این پارامتر در ASP.NET MVC به صورت زیر میتواند باشد:
[AsyncTimeout(8000)] public async Task<ActionResult> Index(CancellationToken cancellationToken)
- برای اجرا و دریافت نتیجهی متدهای Async دار EF، نیاز است از واژهی کلیدی await استفاده گردد.
استفاده کننده نیز میتواند متد addUserAsync را به صورت زیر فراخوانی کند:
var user = await addUserAsync(); Console.WriteLine("user id: {0}", user.Id);
شبیه به همین اعمال را نیز جهت به روز رسانی و یا حذف اطلاعات خواهیم داشت:
private async Task<User> updateAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) user1.Name = "Vahid N."; await context.SaveChangesAsync(cancellationToken); return user1; } } private async Task<int> deleteAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) context.Users.Remove(user1); return await context.SaveChangesAsync(cancellationToken); } }
کدهای Async تقلبی!
به قطعه کد ذیل دقت کنید:
public async Task<List<TEntity>> GetAllAsync() { return await Task.Run(() => _tEntities.ToList()); }
به این نوع متدها که از Task.Run برای فراخوانی متدهای همزمان قدیمی مانند ToList جهت Async جلوه دادن آنها استفاده میشود، کدهای Async تقلبی میگویند! این عملیات هر چند در یک ترد دیگر انجام میشود اما هم سربار ایجاد یک ترد جدید را به همراه دارد و هم عملیات ToList آن کاملا blocking است.
معادل صحیح Async واقعی این عملیات را در ذیل مشاهده میکنید:
private async Task<List<User>> getUsersAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { return await context.Users.ToListAsync(cancellationToken); } }
برای مثال پشت صحنهی متد الحاقی SaveChangesAsync به یک چنین متدی ختم میشود:
internal override async Task<long> ExecuteAsync( //... rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); //...
و یا برای شبیه سازی ToListAsync با ADO.NET 4.5 و استفاده از متدهای Async واقعی آن، به یک چنین کدهایی نیاز است:
var connectionString = "........"; var sql = @"......""; var users = new List<User>(); using (var cnx = new SqlConnection(connectionString)) { using (var cmd = new SqlCommand(sql, cnx)) { await cnx.OpenAsync(); using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) { while (await reader.ReadAsync()) { var user = new User { Id = reader.GetInt32(0), Name = reader.GetString(1), }; users.Add(user); } } } }
محدودیت پردازش موازی اعمال در EF
در متد ذیل، دو Task غیرهمزمان تعریف شدهاند و سپس با await Task.WhenAll درخواست اجرای همزمان و موازی آنها را کردهایم:
// multiple operations private static async Task loadAllAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var task1 = context.Users.ToListAsync(cancellationToken); var task2 = context.BlogPosts.ToListAsync(cancellationToken); await Task.WhenAll(task1, task2); // use task1.Result } }
An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll Additional information: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
در این قسمت مدلهای مربوط به بخش انجمن را تکمیل کرده و همچنین سیستم نظرسنجی را نیز بررسی خواهیم کرد.
همکاران این قسمت:
سلمان معروفی
سید مجبتی حسینی
مدل پستهای انجمن
/// <summary> /// Represents The Post of Forum /// </summary> public class ForumPost : AuditBaseEntity { #region Ctor /// <summary> /// create one instance of <see cref="ForumPost"/> /// </summary> public ForumPost() { CreatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets body of this post /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets Count of this post's reports /// </summary> public virtual int ReportsCount { get; set; } /// <summary> /// gets or sets information of User-Agent /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets rating values /// <remarks>is a complex type</remarks> /// </summary> public virtual Rating Rating { get; set; } /// <summary> /// gets or sets author's ip address /// </summary> public virtual string CreatorIp { get; set; } /// <summary> /// gets or sets status of this post /// </summary> public virtual ForumPostStatus Status { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets ParentPost of this post /// </summary> public virtual ForumPost Reply { get; set; } /// <summary> /// gets or sets ParentPost's Id of this post /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets /// </summary> public virtual ICollection<ForumPost> Children { get; set; } /// <summary> /// gets or sets Topic That Associated with this Post /// </summary> public virtual ForumTopic Topic { get; set; } /// <summary> /// gets or sets Id of Topic That Associated with this Post /// </summary> public virtual long TopicId { get; set; } /// <summary> /// get or sets Histories of this Post's Updates /// </summary> public virtual ICollection<ForumPostHistory> Histories { get; set; } /// <summary> /// gets or sets Forum that this post created in it . used for retrive posts count /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets id of Forum that this post created in it . used for retrive posts count /// </summary> public virtual long ForumId { get; set; } #endregion } public enum ForumPostStatus { /* 0 - approved, 1 - pending, 2 - spam, -1 - trash */ [Display(Name = "تأیید شده")] Approved = 0, [Display(Name = "در انتظار بررسی")] Pending = 1, [Display(Name = "جفنگ")] Spam = 2, [Display(Name = "زباله دان")] Trash = -1 }
مدل بالا مشخص کنندهی پستهایی که در پاسخ به تاپیکها ارسال میشوند، میباشد. ساختار درختی آن به منظور امکان پاسخ به پستها در نظر گرفته شده است. در هر تاپیک چندین پست ارسال میشود که اولین پست ارسال شده، همان محتوای اصلی تاپیک میباشد. بدین منظور خصوصیت Topic را در مدل بالا تعریف کردهایم. برای این پستهای ارسالی امکان امتیاز دهی و اخطار دادن نیز خواهیم داشت که به ترتیب خصوصیات Rating و ReportsCount (بحث شده در مقالات قبل) را در مدل بالا تعریف کردهایم. خصوصیت Status به منظور اعمال مدیریتی در نظر گرفته شده است که از نوع ForumPostStatus میباشد و در بالا تعریف آن نیز آمده است.
نکته : خصوصیتی از نوع مدل Forum نیز در مدل بالا تعریف شده است. هدف از آن افزایش سرعت ویرایش خصوصیات ApprovedPostsCount و UnApprovedPostsCount موجود در مدل Forum میباشد. در واقع هنگام درج پست جدید یا حذف پستی و یا ... ، لازم است خصوصیات مذکور به روز شوند.
علاوه بر این موارد ، لازم است تاریخچهی تغییرات پستهای ارسالی را هم نگهداری کرد تا در صورت نیاز به آنها استناد کنیم. از طرفی پستهای ارسالی را میتوان چندین بار ویرایش کرد. به همین دلیل خصوصیت Histories را که لیستی از مدل ForumPostHistory میباشد، در مدل بالا تعریف کردهایم.
مدل تاریخچهی تغییرات پست
/// <summary> /// Represents History Of Post's Updates /// </summary> public class ForumPostHistory { #region Ctor /// <summary> /// create one instance of <see cref="ForumPostHistory"/> /// </summary> public ForumPostHistory() { Id = SequentialGuidGenerator.NewSequentialGuid(); CreatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets Identifier of this history /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets Reason of update /// </summary> public virtual string Reason { get; set; } /// <summary> /// gets or sets DateTime that this record added /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets body of this post /// </summary> public virtual string Body { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Post /// </summary> public virtual ForumPost Post { get; set; } /// <summary> /// gets or sets Id Of Post /// </summary> public virtual long PostId { get; set; } /// <summary> /// gets or sets User that modified this Record /// </summary> public virtual User Modifier { get; set; } /// <summary> /// gets or sets if of User that modified this Record /// </summary> public virtual long ModifierId { get; set; } #endregion }
اگر خصوصیت ModifyLocked مربوط به مدل ForumPost که آن را از کلاس پایه AuditBaseEntity به ارث برده است، دارای مقدار true باشد، این امکان وجود خواهد داشت تا بتوان پست مورد نظر را ویرایش کرده و اطلاعات قبلی، در قالب یک رکورد در جدول حاصل از مدل بالا ثبت شوند.
- Reason : دلیل این ویرایش به عمل آماده
- Body : محتوای پست یا تاپیک
- Modifier : کاربر انجام دهندهی این ویرایش
- CreatedOn : زمانی که این ویرایش انجام شده است
مدل ردیابی انجمن ها
public class ForumTracker { #region Ctor /// <summary> /// create one instance of <see cref="ForumTracker"/> /// </summary> public ForumTracker() { LastMarkedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets DateTime Of Las Visit by User /// </summary> public virtual DateTime LastMarkedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets Forum that Tracked /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets Id of Forum tath Tracked /// </summary> public virtual long ForumId { get; set; } /// <summary> /// gets or sets User that tracked The forum /// </summary> public virtual User Tracker { get; set; } /// <summary> /// gets or sets Id Of User that Tracked the forum /// </summary> public virtual long TrackerId { get; set; } #endregion } public class ForumTopicTracker { #region Ctor /// <summary> /// create one instance of <see cref="ForumTopicTracker"/> /// </summary> public ForumTopicTracker() { LastVisitedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets DateTime Of Las Visit by User /// </summary> public virtual DateTime LastVisitedOn { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets topc that Tracked /// </summary> public virtual ForumTopic Topic { get; set; } /// <summary> /// gets or sets Id of topic that Tracked /// </summary> public virtual long TopicId { get; set; } /// <summary> /// gets or sets User that tracked The topic /// </summary> public virtual User Tracker { get; set; } /// <summary> /// gets or sets Id Of User that Tracked the topic /// </summary> public virtual long TrackerId { get; set; } /// <summary> /// gets or sets Forum /// </summary> public virtual Forum Forum { get; set; } /// <summary> /// gets or sets Identifier of Forum . used for delete /// </summary> public virtual long ForumId { get; set; } #endregion }
از مدل ForumTopicTracker هم برای مشخص کردن اینکه کاربر کدام تاپیک را و در چه تاریخی آخرین بار مشاهده کرده است، کمک میگیریم. برای این منظور از خصوصیت LastVisitedOn استفاده میشود.
البته نیاز است هنگام واکشی انجمنها و تاپیکها، یکسری بررسیهایی را بر اساس این جداول انجام داد که تشریح این بررسیها را قصد دارم هنگام پیاده سازی سیستم انجام دهم.
این قسمت از کار کمی پیچیده است و برای خودم نیز چالش داشت. سعی کردم انجمنهای سورس باز PHP را بررسی کنم تا در نهایت به تحلیل بالا دست یافتم. مدلهای ارائه شده انجمن تا این قسمت، نیازهای مورد نظر ما را برآورده خواهند کرد.
مدل سیستم نظرسنجی
public class Poll : BaseContent { #region Ctor /// <summary> /// create one instance of <see cref="Poll"/> /// </summary> public Poll() { Rating = new Rating(); PublishedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or set Date that this Poll will Expire /// </summary> public virtual DateTime? ExpireOn { get; set; } /// <summary> ///indicating this poll allow to select multi item /// </summary> public virtual bool IsMultiSelect { get; set; } /// <summary> /// gets or sets Count of this poll's votes /// </summary> public virtual long VotesCount { get; set; } /// <summary> /// indicate this Poll is approved by admin if Poll.Moderate==true /// </summary> public virtual bool IsApproved { get; set; } #endregion #region NavigationProperties /// <summary> /// get or set comments of this poll /// </summary> public virtual ICollection<PollComment> Comments { get; set; } /// <summary> /// get or set Options Of Poll For selection /// </summary> public virtual ICollection<PollOption> Options { get; set; } /// <summary> /// get or set Users List That vote for this poll /// </summary> public virtual ICollection<User> Voters { get; set; } #endregion }
- ExpireOn : زمان اتمام فرصت رای دهی که اگر نال باشد در آن صورت زمان انقضا نخواهد داشت.
- IsMultiSelect : اگر انتخاب چندگزینهای مجاز باشد، این خصوصیت، با مقدار true مقدار دهی میشود.
- VotesCount : به منظور افزایش کارآیی در نظر گرفته شده است و تعداد کل رایهای داده شدهی به نظرسنجی را در بر میگیرد.
- Voters : برای جلوگیری از رای دهی چند بارهی کاربر به یک نظرسنجی، یک ارتباط چند به چند بین کاربر و نظرسنجی برقرار کردهایم. هر کاربر به چند نظر سنجی میتواند پاسخ دهد و به هر نظرسنجی توسط چندین کاربر رای داده میشود.
- PollOptions : هر نظر سنجی تعدادی گزینهی انتخابی هم خواهد داشت که برای همین منظور و اعمال ارتباط یک به چند بین نظرسنجی و گزینههای انتخابی، لیستی از PollOption را در مدل بالا تعریف کردهایم.
مدل گزینههای نظرسنجی
public class PollOption { #region Properties /// <summary> /// gets or sets identifier of this polloption /// </summary> public virtual long Id { get; set; } /// <summary> /// gets or sets Title of this polloption /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets count of votes /// </summary> public virtual long VotesCount { get; set; } /// <summary> /// gets or sets Description of this Option for more details /// </summary> public virtual string Description { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets the poll that assosiated with this Polloption /// </summary> public virtual Poll Poll { get; set; } /// <summary> /// gets or sets the id of poll that assosiated with this Polloption /// </summary> public virtual long PollId { get; set; } #endregion }
- Title: عنوان گزینهی مورد نظر
- Description: توضیح بیشتر برای گزینهی مورد نظر
- VotesCount: تعداد باری که یک گزینه در نظر سنجی انتخاب شده است.
مدل نظرات سیستم نظرسنجی
public class PollComment : BaseComment { #region Ctor public PollComment() { CreatedOn = DateTime.Now; Rating = new Rating(); } #endregion #region NavigationProperties /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual long? ReplyId { get; set; } /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual PollComment Reply { get; set; } /// <summary> /// gets or sets body of blog poll's comment /// </summary> public virtual ICollection<PollComment> Children { get; set; } /// <summary> /// gets or sets poll that this comment sent to it /// </summary> public virtual Poll Poll { get; set; } /// <summary> /// gets or sets poll'Id that this comment sent to it /// </summary> public virtual long PollId { get; set; } #endregion }
در مقالهی بعد به بررسی سیستم پیام رسانی و همچنین بخشی از سیستم تحت عنوان Collections (امکان ساخت گروههای شخصی برای انتشار مطالب خود (توسط کاربران) با اعمال دسترسیهای مختلف) خواهیم پرداخت.
نتیجه تا این قسمت
Summary
JavaScript is a language written for websites to run in the client’s browser.
AJAX is a way for JavaScript to request data from a server without refreshing the page or blocking the application.
jQuery is a JavaScript library built to automate and simplify common web tasks like AJAX or animation.
Angular is a hip JavaScript framework which is made for building large, single-page web applications.
Node.js allows JavaScript to be run without a browser, and is commonly used to run web servers.
ارسال کوئری استرینگها به همراه عملیات Ajax در jQuery
ضمنا بحث وب گرید، خارج از موضوع مطلب جاری است. از پارامتر ajaxUpdateContainerId آن افزونه خاص باید برای مدیریت مسایل paging و sorting استفاده کنید.
برای استفاده از FTP نیاز به یک اکانت FTP در سایت مورد نظر بهمراه دسترسی به پوشه ای مشخص میباشد.
برای مثال ما یک اکانت FTP در سایت dotnettip.info داریم که به پوشه upload دسترسی داره.
ابتدا در فایل Web.config و در بین تگ های appSettings مقادیر زیر را برای دسترسی به اکانت و نام کاربری و رمز عبور ذخیره میکنیم.
<add key="FtpAddress" value="ftp://ftp.dotnetips.info" /> <add key="FtpUser" value="uploadcenter" /> <add key="FtpPass" value="123123" /> <add key="FolderPath" value="~/Upload/" />
در ادامه یک کلاس در App_code پروژه خود با نام FTPHelper ایجاد میکنیم و کد زیر را در آن قرار میدهیم:
تکه کد بالا برای ست کردن مقادیر نام کاربری و رمز عبور و آدرس FTP در کلاس مذکور که بصورت پیشفرض از web.config پر میشود ایجاد و بکار خواهد رفت.
using System.Net; using System.IO; using System.Configuration; public class FtpHelper { public FtpHelper() { //Default Value Set From Application _hostname = ConfigurationManager.AppSettings.GetValues("FtpAddress")[0]; _username = ConfigurationManager.AppSettings.GetValues("FtpUser")[0]; _password = ConfigurationManager.AppSettings.GetValues("FtpPass")[0]; } #region "Properties" private string _hostname; /// <summary> /// Hostname /// </summary> /// <value></value> /// <remarks>Hostname can be in either the full URL format /// ftp://ftp.myhost.com or just ftp.myhost.com /// </remarks> public string Hostname { get { if (_hostname.StartsWith("ftp://")) { return _hostname; } else { return "ftp://" + _hostname; } } set { _hostname = value; } } private string _username; /// <summary> /// Username property /// </summary> /// <value></value> /// <remarks>Can be left blank, in which case 'anonymous' is returned</remarks> public string Username { get { return (_username == "" ? "anonymous" : _username); } set { _username = value; } } private string _password; public string Password { get { return _password; } set { _password = value; } } #endregion }
سپس فضای نامهای زیر را در کلاس خود قرار میدهیم.
using System.Net; using System.IO;
public static bool Upload(string fileUrl) { if (File.Exists(fileUrl)) { FtpHelper ftpClient = new FtpHelper(); string ftpUrl = ftpClient.Hostname + System.IO.Path.GetFileName(fileUrl); FtpWebRequest ftp = (FtpWebRequest)FtpWebRequest.Create(ftpUrl); ftp.Credentials = new NetworkCredential(ftpClient.Username, ftpClient.Password); ftp.KeepAlive = true; ftp.UseBinary = true; ftp.Timeout = 3600000; ftp.KeepAlive = true; ftp.Method = WebRequestMethods.Ftp.UploadFile; const int bufferLength = 102400; byte[] buffer = new byte[bufferLength]; int readBytes = 0; //open file for reading using (FileStream fs = File.OpenRead(fileUrl)) { try { //open request to send using (Stream rs = ftp.GetRequestStream()) { do { readBytes = fs.Read(buffer, 0, bufferLength); fs.Write(buffer, 0, readBytes); } while (!(readBytes < bufferLength)); rs.Close(); } } catch (Exception) { //Optional Alert for Exeption To Application Layer //throw (new ApplicationException("بارگذاری فایل با خطا رو به رو شد")); } finally { //ensure file closed //fs.Close(); } } ftp = null; return true; } return false; }
تکه کد بالا فایل مورد نظر را در صورت وجود به صورت تکههای 100 کیلوبایتی بر روی ftp بارگذاری میکند، که میتوانید مقدار آنرا نیز تغییر دهید.
اینکار باعث افزایش سرعت بارگذاری در فایلهای با حجم بالا برای بارگذاری میشود.
در بخشهای بعدی نحوه ایجاد پوشه ، حذف فایل ، حذف پوشه و دانلود فایل از روی FTP را بررسی خواهیم کرد.
صفحه بندی پویا در Entity Framework
در ادامه مثالی را بررسی خواهیم کرد که بر اساس ردیفهای گزارش آن، یک نمودار، به انتهای گزارش اضافه خواهد شد.
کدهای کامل این مثال را در ذیل مشاهده میکنید:
using System; using System.Collections.Generic; using PdfReportSamples.Models; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; using PdfRpt.FluentInterface; namespace PdfReportSamples.ChartImage { public class ChartImagePdfReport { public IPdfReportData CreatePdfReport() { var chart = new MSChartHelper { AxisXTitle = "User", AxisYTitle = "Balance", ChartTitle = "Users Balance", AxisTitleFont = new System.Drawing.Font("Tahoma", 12f), LabelStyleFont = new System.Drawing.Font("Tahoma", 10f), ChartTitleFont = new System.Drawing.Font("Arial", 16f, System.Drawing.FontStyle.Bold), LegendsFont = new System.Drawing.Font("Tahoma", 12f) }; return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.RightToLeft); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" }); }) .DefaultFonts(fonts => { fonts.Path(string.Format("{0}\\fonts\\irsans.ttf", AppPath.ApplicationPath), string.Format("{0}\\fonts\\verdana.ttf", Environment.GetEnvironmentVariable("SystemRoot"))); }) .PagesFooter(footer => { footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("گزارش جدید ما"); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); }) .MainTableDataSource(dataSource => { var listOfRows = new List<User>(); for (var i = 0; i < 7; i++) { listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = (i * 50) + 1000 }); } dataSource.StronglyTypedList(listOfRows); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); events.DocumentOpened(args => { chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250); }); events.RowAdded(args => { if (args.RowType == RowType.DataTableRow) { var name = args.TableRowData.GetValueOf<User>(x => x.Name); if (name == null) return; var balance = args.TableRowData.GetValueOf<User>(x => x.Balance); if (balance == null) return; chart.AddXY(name, balance); } }); events.DocumentClosing(args => { chart.AddChartToPage(args.PdfDoc); chart.FreeResources(); }); }) .MainTableSummarySettings(summary => { summary.OverallSummarySettings("جمع"); summary.PreviousPageSummarySettings("نقل از صفحه قبل"); }) .MainTableColumns(columns => { columns.AddColumn(column => { column.PropertyName("rowNo"); column.IsRowNumber(true); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(0); column.Width(1); column.HeaderCell("ردیف", captionRotation: 90); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Id); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(1); column.Width(2); column.HeaderCell("شماره"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Name); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(2); column.HeaderCell("نام"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.LastName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(3); column.HeaderCell("نام خانوادگی"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Balance); column.HeaderCell("موجودی"); column.ColumnItemsTemplate(template => { template.TextBlock(); template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); column.Width(2); column.AggregateFunction(aggregateFunction => { aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum); aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(4); }); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptChartSample.pdf")); } } }
using System.Drawing; using System.IO; //It's part of the .NET 4.0+ now. using System.Windows.Forms.DataVisualization.Charting; using iTextSharp.text; using iTextSharp.text.pdf; using PdfRpt.Core.Helper; namespace PdfReportSamples.ChartImage { public class MSChartHelper { // MS Chart learning tutorials: // http://weblogs.asp.net/scottgu/archive/2010/02/07/built-in-charting-controls-vs-2010-and-net-4-series.aspx Chart _chart; public System.Drawing.Font AxisTitleFont { set; get; } public string AxisXTitle { set; get; } public string AxisYTitle { set; get; } public string ChartTitle { set; get; } public System.Drawing.Font ChartTitleFont { set; get; } public System.Drawing.Font LabelStyleFont { set; get; } public System.Drawing.Font LegendsFont { set; get; } public void AddChartToPage(Document pdfDoc, int scalePercent = 100, float spacingBefore = 20, float spacingAfter = 10, float widthPercentage = 80) { using (var chartimage = new MemoryStream()) { _chart.SaveImage(chartimage, ChartImageFormat.Bmp); //BMP gives the best compression result var iTextSharpImage = PdfImageHelper.GetITextSharpImageFromByteArray(chartimage.GetBuffer()); iTextSharpImage.ScalePercent(scalePercent); iTextSharpImage.Alignment = Element.ALIGN_CENTER; var table = new PdfPTable(1) { WidthPercentage = widthPercentage, SpacingBefore = spacingBefore, SpacingAfter = spacingAfter }; table.AddCell(iTextSharpImage); pdfDoc.Add(table); } } public void AddXY(object xValue, params object[] yValue) { _chart.Series[0].Points.AddXY(xValue, yValue); } public void ChartInit(int width, int height) { _chart = new Chart { Width = width, Height = height, AntiAliasing = AntiAliasingStyles.All, TextAntiAliasingQuality = TextAntiAliasingQuality.High, Palette = ChartColorPalette.BrightPastel, BackColor = ColorTranslator.FromHtml("#F3DFC1"), BackGradientStyle = GradientStyle.TopBottom }; setBorder(); setTitles(); setChartAreas(); setLegends(); setSeries(); } public void FreeResources() { if (_chart != null && !_chart.IsDisposed) _chart.Dispose(); } private void setBorder() { _chart.BorderSkin.SkinStyle = BorderSkinStyle.Emboss; _chart.BorderlineWidth = 2; _chart.BorderlineColor = Color.FromArgb(181, 64, 1); _chart.BorderlineDashStyle = ChartDashStyle.Solid; } private void setChartAreas() { _chart.ChartAreas.Add("ChartArea1"); _chart.ChartAreas[0].AxisX.Title = AxisXTitle; _chart.ChartAreas[0].AxisY.Title = AxisYTitle; _chart.ChartAreas[0].AxisX.TitleFont = AxisTitleFont; _chart.ChartAreas[0].AxisY.TitleFont = AxisTitleFont; _chart.ChartAreas[0].AxisX.LabelStyle.Font = LabelStyleFont; _chart.ChartAreas[0].AxisX.LabelStyle.Angle = -90; _chart.ChartAreas[0].BackColor = Color.White; _chart.ChartAreas[0].AxisX.LineColor = Color.FromArgb(64, 64, 64); _chart.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.FromArgb(64, 64, 64); _chart.ChartAreas[0].AxisY.LineColor = Color.FromArgb(64, 64, 64); _chart.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.FromArgb(64, 64, 64); } private void setLegends() { _chart.Legends.Add("Default"); _chart.Legends[0].LegendStyle = LegendStyle.Row; _chart.Legends[0].IsTextAutoFit = false; _chart.Legends[0].DockedToChartArea = "ChartArea1"; _chart.Legends[0].Docking = Docking.Bottom; _chart.Legends[0].IsDockedInsideChartArea = false; _chart.Legends[0].BackColor = Color.Transparent; _chart.Legends[0].Font = LegendsFont; } private void setSeries() { _chart.Series.Add(""); _chart.Series[0].ChartType = SeriesChartType.Column; _chart.Series[0].Palette = ChartColorPalette.EarthTones; _chart.Series[0].IsValueShownAsLabel = true; _chart.Series[0].IsVisibleInLegend = false; } private void setTitles() { _chart.Titles.Add(ChartTitle); _chart.Titles[0].Font = ChartTitleFont; _chart.Titles[0].TextStyle = TextStyle.Shadow; _chart.Titles[0].ShadowOffset = 3; _chart.Titles[0].ShadowColor = Color.FromArgb(32, 0, 0); _chart.Titles[0].Alignment = ContentAlignment.TopCenter; _chart.Titles[0].ForeColor = Color.FromArgb(26, 59, 105); } } }
توضیحات:
- استفاده از MSChart در اینجا از این جهت مناسب است که فراگیری کار کردن با آن عمومی بوده و در پروژههای وب و ویندوز کاربرد دارد و احتمالا هم اکنون با نحوه کارکردن با آن آشنا هستید، زیرا از سال 2010 به دات نت اضافه شده است.
- در این بین تنها متد جدید و مهم کلاس MSChartHelper، متد AddChartToPage آن است. به کمک متد chart.SaveImage میتوان تصویر نهایی معادل یک نمودار را در حافظه ذخیره کرد. سپس با استفاده از متد PdfImageHelper.GetITextSharpImageFromByteArray، این تصویر موجود در حافظه را به معادل قابل استفاده آن در iTextSharp تبدیل کرده و به صفحه اضافه خواهیم کرد.
- در کدهای اصلی تولید گزارش، مقدار دهی کلاس کمکی MSChartHelper در قسمت رخدادهای قابل استفاده PdfReport در متد MainTableEvents آن انجام شده است:
در روال رویدادگردان DocumentOpened، بر اساس عرض واقعی صفحه، عرض نمودار را مشخص میکنیم:
events.DocumentOpened(args => { chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250); });
events.RowAdded(args => { if (args.RowType == RowType.DataTableRow) { var name = args.TableRowData.GetValueOf<User>(x => x.Name); if (name == null) return; var balance = args.TableRowData.GetValueOf<User>(x => x.Balance); if (balance == null) return; chart.AddXY(name, balance); } });
events.DocumentClosing(args => { chart.AddChartToPage(args.PdfDoc); chart.FreeResources(); });