طراحی دو صفحهای وب با React
In this post, we’ll look at how we can use one of the most popular libraries, React, to build a foldable web experience.
آموزش JavaScript در 30 روز
اگر کتابخانهی Google.Apis.Analytics.v3 را بر روی یک سیستم دات نت 4 اجرا کنید، احتمالا خطای ذیل را دریافت خواهید کرد:
Could not load type 'System.Net.HttpStatusCode' from assembly System.Net
http://nopcommerce.codeplex.com/SourceControl/latest#src/Libraries/Nop.Data/EfRepository.cs
و همینطور جهت تست پذیری پروژه، راه حلهای اشاره شده را پیاده سازی کرده.
*در دنیای NoSql پیاده سازیهای متفاوتی از دیتابیسها انجام شده است و هر دیتابیس، ویژگیها و مزایا و معایب خاص خودش را دارد. باید قبول کرد که همیشه و همه جا نمیتوان از یک پایگاه داده NoSql مشخص استفاده نماییم (دلایلی نظیر محدودیتهای License، هزینه پیاده سازی و...). در نتیجه بررسی یک دیتابیس دیگر با شرایط و توانمندیهای خاص آن خالی از سود نیست.
» این دیتاییس در گروه Graph databasesها قرار دارد و از زبان SPARQL (بخوانید Sparkle) برای کوئری گرفتن و کار با دادهها بهره میبرد؛
» متن باز و رایگان است
» پشتیبانی از دات نت چهار به بعد؛
» قابل استفاده در Mobile Application - Windows phone 7 , 8؛
» بدون شما (Schema Less) و با قابیلت ذخیره سازی triple و به فرمت RDF
» پشتیبانی از Linq و OData؛
» پشتیبانی از تراکنشها ؛
» پیاده سازی مدل برنامه به صورت Code First؛
» سرعت بالا جهت ذخیره سازی و لود اطلاعات؛
» نیاز به پیکربندیهای خاص جهت پیاده سازی ندارد؛
» ارائه افزونه رایگان Polaris جهت کوئری گفتن و نمایش Visual داده ها.
و غیره که در ادامه با آنها آشنا خواهید شد.
در B*Db دو روش برای ذخیره سازی اطلاعات وجود دارد:
» Append Only : در این روش تمامی تغییرات (عملیات نوشتن) در انتهای فایل index اضافه خواهد شد. این روش مزایای زیر را به دنبال خواهد داشت:
- عملیات نوشتن هیچگاه عملیات خواندن اطلاعات را block نمیکند. در
نتیجه هر تعداد عملیات خواندن فایل (منظور اجرای کوئریهای SPQRL است) میتواند به صورت موازی بر روی Storeها اجرا شود.
- به دلیل اینکه عمل ویرایش واقعی هیچ گاه انجام نمیشود (دادهها فقط اضافه خواهند شد) همیشه میتوانید وضعیت Store را به حالتهای قبلی بازگردانید.
- عملیات نوشتن اطلاعات بسیار سریع خواهد بود.
نکته ای که باید به آن دقت داشت این است که فقط در هنگام ساخت Storeها میتوانید نوع ذخیره سازی آن را تعیین نمایید، بعد از ساخت Store امکان سوئیچ بین حالات امکان پذیر نیست.
همان طور که پیشتر گفته شد B*Db برای ذخیره سازی اطلاعات از سند RDF بهره میبرد. البته با RDF Syntaxهای متفاوت :
هم چنین در B*Db چهار روش برای دست یابی با دادهها (پیاده سازی عملیات CRUD) وجود دارد از قبیل:
» B*Db EntityFramewok
» Data Object Layer
» RDF Client Api
» Dynamic API
که هر کدام در طی پستهای متفاوت بررسی خواهد شد.
بررسی یک مثال با روش B*Db EntityFramework
برای شروع ابتدا یک پروژه جدید از نوع Console Application ایجاد کنید. سپس از طریق Nuget اقدام به نصب Package زیر نمایید:
pm> install-Package BirghtStarDb
PM> Install-Package BirghtStarDbLibs
بعد از نصب پکیجهای بالا یک فایل Text Template با نام MyEntityContext.tt نیز به پروژه افزوده خواهد شد. این فایل جهت تولید خودکار مدلهای برنامه استفاده میشود. اما برای این کار لازم است به ازای هر مدل ابتدا یک اینترفیس ایجاد نمایید. برای مثال:
[Entity] public interface IBook { public int Code { get; set; } public string Title { get; set; } }
» نیاز به ایجاد یک خاصیت به عنوان Id وجود ندارد. به صورت پیش فرض خاصیت Id با نوع string برای هر مدل پیاده سازی میشود. اما اگر قصد دارید این نام خاصیت را تغییر دهید میتوانید به صورت زیر عمل کنید:
[Entity] public interface IBook { [Identifier] public string MyId { get; } public int Code { get; set; } public string Title { get; set; } }
استفاده از اینترفیس برای ساخت مدل انعطاف پذیری بالایی را در اختیار ما قرار میدهد که بعدا مفصل بحث خواهد شد. برای عملیات درج داده میتوان به صورت زیر عمل کنید:
MyEntityContext context = new MyEntityContext("type=embedded;storesdirectory=c:\brightstar;storename=test"); var book = context.Books.Create(); book.Code = 1; book.Title = "Test"; context.Books.Add(book); context.SaveChanges();
»embedded : در این حالت از طریق آدرس فیزیکی فایل مورد نظر میتوان یک Connection ایجاد کرد.
»rest : یا استفاده از HTTP یا HTTPS میتوان به سرویس B*Db دسترسی داشت.
»dotNetRdf : برای ارتباط با یک Store دیگر با استفاده از اتصال دهندههای DotNetRDf.
»sparql : اتصال به منبع داده ای دیگر با استفاده از پروتکل SPARQL
در هنگام ایجاد اتصال باید نوع مورد نظر را از حتما تعیین نمایید. با استفاده از storedirctory مکان فیزیکی فایل تعیین خواهد شد.
dotnet tool install -g upgrade-assistant
dotnet tool update -g upgrade-assistant
- Back up project
- Convert project file to SDK style
- Clean up NuGet package references
upgrade-assistant upgrade MyProject.csproj
- اگر پروژه از جنس Library باشد، فایل csproj را به شکل زیر درآورید:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net472</TargetFramework> <OutputType>Library</OutputType> </PropertyGroup> . . . </Project>
- اگر پروژه از جنس Console باشد، فایل csproj را به شکل زیر درآورید:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net472</TargetFramework> <OutputType>Exe</OutputType> <ApplicationIcon>logo.ico</ApplicationIcon> <AppConfig>App.config</AppConfig> </PropertyGroup> . . . </Project>
- اگر پروژه از جنس Web باشد، فایل csproj را به شکل زیر درآورید. احتمالاً پیچیدهترین و سختترین فایلها، متعلق به پروژههای وب باشد. اگر دقت کنید نوع SDK از نوع MSBuild.SDK.SystemWeb میباشد. نسخه این SDK ممکن است در زمانیکه شما در حال خواندن این مطلب میباشد آپدیت شده باشد و بهتر است قبل از استفاده، آخرین نسخه را از نیوگت برداشت کنید. (باید ذکر کنم که این hack کوچک را از یک comment در issueهای گیتهاب پیدا کردم.)
<Project Sdk="MSBuild.SDK.SystemWeb/4.0.54"> <PropertyGroup> <OutputType>Library</OutputType> <TargetFramework>net472</TargetFramework> <AppConfig>Web.config</AppConfig> </PropertyGroup> . . . </Project>
بروز رسانی موجودیتهای منفصل توسط WCF
سناریویی را در نظر بگیرید که در آن عملیات CRUD توسط WCF پیاده سازی شده اند و دسترسی دادهها با مدل Code-First انجام میشود. فرض کنید مدل اپلیکیشن مانند تصویر زیر است.
همانطور که میبینید مدل ما متشکل از پستها و نظرات کاربران است. برای ساده نگاه داشتن مثال جاری، اکثر فیلدها حذف شده اند. مثلا متن پست ها، نویسنده، تاریخ و زمان انتشار و غیره. میخواهیم تمام کد دسترسی دادهها را در یک سرویس WCF پیاده سازی کنیم تا کلاینتها بتوانند عملیات CRUD را توسط آن انجام دهند. برای ساختن این سرویس مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع Class Library بسازید و نام آن را به Recipe2 تغییر دهید.
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- سه کلاس با نامهای Post, Comment و Recipe2Context به پروژه اضافه کنید. کلاسهای Post و Comment موجودیتهای مدل ما هستند که به جداول متناظرشان نگاشت میشوند. کلاس Recipe2Context آبجکت DbContext ما خواهد بود که بعنوان درگاه عملیاتی EF عمل میکند. دقت کنید که خاصیتهای لازم WCF یعنی DataContract و DataMember در کلاسهای موجودیتها بدرستی استفاده میشوند. لیست زیر کد این کلاسها را نشان میدهد.
[DataContract(IsReference = true)] public class Post { public Post() { comments = new HashSet<Comments>(); } [DataMember] public int PostId { get; set; } [DataMember] public string Title { get; set; } [DataMember] public virtual ICollection<Comment> Comments { get; set; } } [DataContract(IsReference=true)] public class Comment { [DataMember] public int CommentId { get; set; } [DataMember] public int PostId { get; set; } [DataMember] public string CommentText { get; set; } [DataMember] public virtual Post Post { get; set; } } public class EFRecipesEntities : DbContext { public EFRecipesEntities() : base("name=EFRecipesEntities") {} public DbSet<Post> posts; public DbSet<Comment> comments; }
- یک فایل App.config به پروژه اضافه کنید و رشته اتصال زیر را به آن اضافه نمایید.
<connectionStrings> <add name="Recipe2ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- حال یک پروژه WCF به Solution جاری اضافه کنید. برای ساده نگاه داشتن مثال جاری، نام پیش فرض Service1 را بپذیرید. فایل IService1.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید.
[ServiceContract] public interface IService1 { [OperationContract] void Cleanup(); [OperationContract] Post GetPostByTitle(string title); [OperationContract] Post SubmitPost(Post post); [OperationContract] Comment SubmitComment(Comment comment); [OperationContract] void DeleteComment(Comment comment); }
- فایل Service1.svc.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. بیاد داشته باشید که پروژه Recipe2 را ارجاع کنید و فضای نام آن را وارد نمایید. همچنین کتابخانه EF 6 را باید به پروژه اضافه کنید.
public class Service1 : IService { public void Cleanup() { using (var context = new EFRecipesEntities()) { context.Database.ExecuteSqlCommand("delete from [comments]"); context. Database.ExecuteSqlCommand ("delete from [posts]"); } } public Post GetPostByTitle(string title) { using (var context = new EFRecipesEntities()) { context.Configuration.ProxyCreationEnabled = false; var post = context.Posts.Include(p => p.Comments).Single(p => p.Title == title); return post; } } public Post SubmitPost(Post post) { context.Entry(post).State = // if Id equal to 0, must be insert; otherwise, it's an update post.PostId == 0 ? EntityState.Added : EntityState.Modified; context.SaveChanges(); return post; } public Comment SubmitComment(Comment comment) { using (var context = new EFRecipesEntities()) { context.Comments.Attach(comment); if (comment.CommentId == 0) { // this is an insert context.Entry(comment).State = EntityState.Added); } else { // set single property to modified, which sets state of entity to modified, but // only updates the single property – not the entire entity context.entry(comment).Property(x => x.CommentText).IsModified = true; } context.SaveChanges(); return comment; } } public void DeleteComment(Comment comment) { using (var context = new EFRecipesEntities()) { context.Entry(comment).State = EntityState.Deleted; context.SaveChanges(); } } }
- در آخر پروژه جدیدی از نوع Windows Console Application به Solution جاری اضافه کنید. از این اپلیکیشن بعنوان کلاینتی برای تست سرویس WCF استفاده خواهیم کرد. فایل program.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. روی نام پروژه کلیک راست کرده و گزینه Add Service Reference را انتخاب کنید، سپس ارجاعی به سرویس Service1 اضافه کنید. رفرنسی هم به کتابخانه کلاسها که در ابتدای مراحل ساختید باید اضافه کنید.
class Program { static void Main(string[] args) { using (var client = new ServiceReference2.Service1Client()) { // cleanup previous data client.Cleanup(); // insert a post var post = new Post { Title = "POCO Proxies" }; post = client.SubmitPost(post); // update the post post.Title = "Change Tracking Proxies"; client.SubmitPost(post); // add a comment var comment1 = new Comment { CommentText = "Virtual Properties are cool!", PostId = post.PostId }; var comment2 = new Comment { CommentText = "I use ICollection<T> all the time", PostId = post.PostId }; comment1 = client.SubmitComment(comment1); comment2 = client.SubmitComment(comment2); // update a comment comment1.CommentText = "How do I use ICollection<T>?"; client.SubmitComment(comment1); // delete comment 1 client.DeleteComment(comment1); // get posts with comments var p = client.GetPostByTitle("Change Tracking Proxies"); Console.WriteLine("Comments for post: {0}", p.Title); foreach (var comment in p.Comments) { Console.WriteLine("\tComment: {0}", comment.CommentText); } } } }
Comment: I use ICollection<T> all the time
شرح مثال جاری
ابتدا با اپلیکیشن کنسول شروع میکنیم، که کلاینت سرویس ما است. نخست در یک بلاک {} using وهله ای از کلاینت سرویس مان ایجاد میکنیم. درست همانطور که وهله ای از یک EF Context میسازیم. استفاده از بلوکهای using توصیه میشود چرا که متد Dispose بصورت خودکار فراخوانی خواهد شد، چه بصورت عادی چه هنگام بروز خطا. پس از آنکه وهله ای از کلاینت سرویس را در اختیار داشتیم، متد Cleanup را صدا میزنیم. با فراخوانی این متد تمام دادههای تست پیشین را حذف میکنیم. در چند خط بعدی، متد SubmitPost را روی سرویس فراخوانی میکنیم. در پیاده سازی فعلی شناسه پست را بررسی میکنیم. اگر مقدار شناسه صفر باشد، خاصیت State موجودیت را به Added تغییر میدهید تا رکورد جدیدی ثبت کنیم. در غیر اینصورت فرض بر این است که چنین موجودیتی وجود دارد و قصد ویرایش آن را داریم، بنابراین خاصیت State را به Modified تغییر میدهیم. از آنجا که مقدار متغیرهای int بصورت پیش فرض صفر است، با این روش میتوانیم وضعیت پستها را مشخص کنیم. یعنی تعیین کنیم رکورد جدیدی باید ثبت شود یا رکوردی موجود بروز رسانی گردد. رویکردی بهتر آن است که پارامتری اضافی به متد پاس دهیم، یا متدی مجزا برای ثبت رکوردهای جدید تعریف کنیم. مثلا رکوردی با نام InsertPost. در هر حال، بهترین روش بستگی به ساختار اپلیکیشن شما دارد.
اگر پست جدیدی ثبت شود، خاصیت PostId با مقدار مناسب جدید بروز رسانی میشود و وهله پست را باز میگردانیم. ایجاد و بروز رسانی نظرات کاربران مشابه ایجاد و بروز رسانی پستها است، اما با یک تفاوت اساسی: بعنوان یک قانون، هنگام بروز رسانی نظرات کاربران تنها فیلد متن نظر باید بروز رسانی شود. بنابراین با فیلدهای دیگری مانند تاریخ انتشار و غیره اصلا کاری نخواهیم داشت. بدین منظور تنها خاصیت CommentText را بعنوان Modified علامت گذاری میکنیم. این امر منجر میشود که Entity Framework عبارتی برای بروز رسانی تولید کند که تنها این فیلد را در بر میگیرد. توجه داشته باشید که این روش تنها در صورتی کار میکند که بخواهید یک فیلد واحد را بروز رسانی کنید. اگر میخواستیم فیلدهای بیشتری را در موجودیت Comment بروز رسانی کنیم، باید مکانیزمی برای ردیابی تغییرات در سمت کلاینت در نظر میگرفتیم. در مواقعی که خاصیتهای متعددی میتوانند تغییر کنند، معمولا بهتر است کل موجودیت بروز رسانی شود تا اینکه مکانیزمی پیچیده برای ردیابی تغییرات در سمت کلاینت پیاده گردد. بروز رسانی کل موجودیت بهینهتر خواهد بود.
برای حذف یک دیدگاه، متد Entry را روی آبجکت DbContext فراخوانی میکنیم و موجودیت مورد نظر را بعنوان آرگومان پاس میدهیم. این امر سبب میشود که موجودیت مورد نظر بعنوان Deleted علامت گذاری شود، که هنگام فراخوانی متد SaveChanges اسکریپت لازم برای حذف رکورد را تولید خواهد کرد.
در آخر متد GetPostByTitle یک پست را بر اساس عنوان پیدا کرده و تمام نظرات کاربران مربوط به آن را هم بارگذاری میکند. از آنجا که ما کلاسهای POCO را پیاده سازی کرده ایم، Entity Framework آبجکتی را بر میگرداند که Dynamic Proxy نامیده میشود. این آبجکت پست و نظرات مربوط به آن را در بر خواهد گرفت. متاسفانه WCF نمیتواند آبجکتهای پروکسی را مرتب سازی (serialize) کند. اما با غیرفعال کردن قابلیت ایجاد پروکسیها (ProxyCreationEnabled=false) ما به Entity Framework میگوییم که خود آبجکتهای اصلی را بازگرداند. اگر سعی کنید آبجکت پروکسی را سریال کنید با پیغام خطای زیر مواجه خواهید شد:
The underlying connection was closed: The connection was closed unexpectedly
می توانیم غیرفعال کردن تولید پروکسی را به متد سازنده کلاس سرویس منتقل کنیم تا روی تمام متدهای سرویس اعمال شود.
در این قسمت دیدیم چگونه میتوانیم از آبجکتهای POCO برای مدیریت عملیات CRUD توسط WCF استفاده کنیم. از آنجا که هیچ اطلاعاتی درباره وضعیت موجودیتها روی کلاینت ذخیره نمیشود، متدهایی مجزا برای عملیات CRUD ساختیم. در قسمتهای بعدی خواهیم دید چگونه میتوان تعداد متدهایی که سرویس مان باید پیاده سازی کند را کاهش داد و چگونه ارتباطات بین کلاینت و سرور را سادهتر کنیم.
Portable Class Library برای تولید Assembly مدیریت شده که قابیلت استفاده در اکثر تکنولوژیها نظیر (WP7(Windows Phone 7 و WPF و Silverlight و Windows Store App و حتی XBox را داراست استفاده میشود. این نوع پروژهها میزان استفاده مجدد از کدها رو به حداکثر ممکن میرسونه و در عوض تعداد پروژههای مورد نیاز رو به حداقل. مورد اصلی استفاده از اونها برای تولید Multi-Targeted Application هاست. برای مثال در تولید پروژههای تحت Windows که با تکنولوژی WPF پیاده سازی میشوند پروژه ViewModel و Model رو با این تکنولوژی پیاده سازی میکنند تا در آینده اگر نیاز بود قسمتی از پروژه با استفاده از Silverlight یا Windows Store App انجام بشه بتونیم Model , ViewModel نوشته شده رو به این پروژهها Reference بدیم. تا قبل از برای انجام این کار باید این دو بخش رو دوباره برای هر تکنولوژی بازنویسی میکردیم.
روش استفاده
زمانی که یک پروژه از نوع Portable Class Library رو ایجاد میکنیم باید حداقل 2 یا چند تا از Platformهای مورد نظر رو انتخاب کنیم. به صورت زیر :
بعد از انتخاب گزینه Portable Class Library و زدن کلید Enter فرم زیر ظاهر خواهد شد که در این قسمت باید Platformهای مورد نظر رو انتخاب کنیم.
در اینجا من 2 Platform رو انتخاب کردم. یکی Silverlight و Net 4.5. یعنی این Portable Class Library هم میتونه به پروژه Silverlight و هم به Class library .Net 4.5 رفرنس داده بشه.
حتما میپرسید چه طوری؟
در واقع Microsoft برای انجام این کار فقط یک سری از Referenceهای مشترک بین این 2 Platform رو به پروژه اضافه میکنه و همین موضوع باعث شده کار با این پروژهها نیاز به یکم مهارت داشته باشه.
برای مثال اگر قصد استفاده از تکنولوژی Async&Await رو دارید باید یکم به خودتون زحمت بدید و CTP مورد نظر رو نصب کنید.(حالا اگر از Net4 استفاده میکنید که باید از یک روش دیگه استفاده کنید که در یک مقاله جداگانه براتون توضیح خواهم داد).
به جدول زیر دقت کنید.
Xbox 360 | Windows Phone | Silverlight | Windows Store | .NET Framework | Feature |
√ | √ | √ | √ | √ | Core |
√ | √ | √ | √ | LINQ | |
Only 7.5 | √ | √ | √ | IQueryable | |
√ | √ | Only 4.5 | Dynamic keyword | ||
√ | √ | √ | Managed Extensibility Framework (MEF) | ||
√ | √ | √ | √ | Network Class Library (NCL) | |
√ | √ | √ | √ | Serialization | |
√ | √ | √ | √ | Windows Communication Foundation (WCF) | |
√ | √ | √ |
Only 4.5 | Model-View-View Model (MVVM) | |
√ | √ | Only 4.0.3 and 4.5 | Data annotations | ||
√ | √ | √ | √ | Only 4.0.3 and 4.5 | XLINQ |
در جدول بالا به صورت کامل مشخص شده که در هر پلاتفرم چه چیزی Support میشه. برای مثال Data Annotation فقط در Net4.3 , 4.5 قابل استفاده است.
اسمبلیهای زیر در یک Portable Class Library قابل استفاده هستند.
mscorlib.dll
System.dll
System.Core.dll
System.Xml.dll
System.ComponentModel.Composition.dll
System.Net.dll
System.Runtime.Serialization.dll
System.ServiceModel.dll
System.Xml.Serialization.dll
System.Windows.dll - From Silverlight
پیاده سازی یک مثال عملی
در این مثال قصد دارم یک کلاس برای مدیریت تاریخ شمسی درست کنم. مراحل زیر رو دنبال کنید
یک Portable Class Library به نام Common به پروژه اضافه کنید.
یک کلاس به نام PersianDate به برنامه اضافه کرده به صورت زیر
public class PersianDate<TCalendar> where TCalendar : System.Globalization.Calendar { public PersianDate() { this.CurentCalendar = System.Activator.CreateInstance<TCalendar>(); } public TCalendar CurentCalendar { get; private set; } public string GetDate() { return string.Format( "{0}/{1}/{2}", this.CurentCalendar.GetYear( DateTime.Today ).ToString( "####0000" ) , this.CurentCalendar.GetMonth( DateTime.Today ).ToString( "##00" ) , this.CurentCalendar.GetDayOfMonth( DateTime.Today ).ToString( "##00" ) ); } }
حالا یک پروژه از نوع Console Application به برنامه اضافه کنید و یک Reference به پروژه Common بدید و بعد کلاس زیر رو بنویسید.
public class CustomPersianDate { public CustomPersianDate() { } public static Common.PersianDate<System.Globalization.PersianCalendar> PersianCalendar { get { return _persianCalendar ?? ( _persianCalendar = new Common.PersianDate<System.Globalization.PersianCalendar>() ); } } private static Common.PersianDate<System.Globalization.PersianCalendar> _persianCalendar; }
حالا یک پروژه از نوع Silverlight به برنامه اضافه کنید و یک Reference به پروژه Common بدید و بعد کلاس بالا بدون تغییر بنویسید.
نمای کلی پروژه باید به صورت زیر باشد.
بعد پروژه ConsoleApplication رو به عنوان پروژه StartUp انتخاب کنید و فایل Program رو به صورت زیر تغییر بدید.
class Program { static void Main( string[] args ) { Console.WriteLine( "Today Is ?{0}", CustomPersianDate.PersianCalendar.GetDate() ); Console.ReadLine(); } }
خوب نتیجه به صورت زیر خواهد بود:
برای پروژه Silverlight هم نتیجه قطعا به همین صورت است.
همان طور که دید انتخاب نوع Calendar به خود Applicationها واگذار شد و شما میتونید هر نوع تقویم رو به عنوان TCalendar تعیین کنید.
امیدوارم شما هم مثل من به این تکنولوژی دلچسب علاقه پیدا کرده باشید.