برای آغاز به کار با این دیتابیس ابتدا باید آن را از سایت اصلی دریافت و بر روی سیستم نصب نمایید. متاسفانه سایت مونگو برای کشور ایران محدودیتی قرار داده است و باید از روشهای دیگری آن را دریافت نمایید و بر روی سیستم خود نصب نمایید. نحوه نصب این دیتابیس را میتوانید در مقاله MongoDb#3 مشاهده نمایید.
شاید نیاز باشد بجای کار کردن با محیط کنسول این دیتابیس، با یک محیط گرافیکی شبیه آن چیزی که Raven دارد کار کنید وتغییرات را مشاهده نمایید؛ برای همین به این آدرس رفته و محیط دلخواه خود را انتخاب نمایید.
یک پروژه از نوع کنسول را در ویژوال استادیو ایجاد کنید و سپس درایور رسمی مونگو را از این آدرس یا از طریق nuget نصب نمایید:
Install-Package mongocsharpdriver
ابتدا سه مدل را به شکل زیر ایجاد میکنیم:
public class Author { public ObjectId Id { get; set; } public string Name { get; set; } }
public class Language { public ObjectId Id { get; set; } public string Name { get; set; } }
public class Book { public ObjectId Id { get; set; } public string Title { get; set; } public string ISBN { get; set; } public int Price { get; set; } public List<Author> Authors { get; set; } public Language Language { get; set; } }
نوع ObjectId، نوعی است که توسط مونگو برای مشخص کردن کلید یکتای سند معرفی میشود.
در خطوط اولیه کد زیر، یک شیء از مدل بالا را ساخته و آن را مقداردهی میکنیم:
var book =new Book() { Title = "Gone With Wind", ISBN = "43442424", Price = 50000, Language = new Language() { Name = "Persian" }, Authors = new List<Author>() { new Author() { Name = "Margaret Mitchell" }, new Author() { Name = "Ali Mahboobi (Translator)" }, } };
بعد از آن یک شیء کلاینت از نوع mongoClient میسازیم که نوع خروجی آن یک اینترفیس میباشد که توسط کلاسی از جنس آن مقداردهی شده است. بیشتر خروجیهای مونگو در این کتابخانه از نوع اینترفیس هستند. شیء کلاینت وظیفه دارد تا ارتباط شما را با سرور مونگو برقرار کند:
var client = new MongoClient();
string connectionString = "mongodb://localhost:27017"; MongoClientSettings settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString)); var client = new MongoClient(settings);
در قسمت بعد لازم است که از سرور جاری، دیتابیس خود را دریافت کنیم. در صورتیکه دیتابیس درخواستی وجود نداشته باشد، یک دیتابیس جدید با آن نام ساخته خواهد شد:
var db = client.GetDatabase("publisher");
در مونگو اصطلاحی به نام collection وجود دارد که اسناد در آن قرار گرفته و ارتباط با اسناد از طریق آنها انجام میپذیرد. پس در اینجا قبل از هر کاری باید یک collection را ایجاد کرد و در صورتیکه کالکشن درخواستی وجود نداشته باشد، آن را تولید و ارتباط با آن را برخواهد گرداند.
var collection = db.GetCollection<Book>("books");
در اینجا کالکشنی با نام books با تبدیلاتی بر اساس مدل Book ایجاد میشود. در مرحله بعد لازم است که شیء ایجاد شده بر اساس کلاس مدل را با استفاده از متدهای insert شیء کالکشن، در دیتابیس ارسال کنیم.
شیءهای درج یک سند جدید به دیتابیس حالات مختلفی را دارد: افزودن تک سند، افزودن چند سند و دو مورد قبلی به صورت غیر همزمان میباشند:
collection.InsertOneAsync(book);
فعلا موجودیتهای مؤلفان و زبان به دلیل اینکه سند اختصاصی برای خود ندارند، با صفر پر شدهاند؛ ولی شناسه یکتای سند، مقدار خود را گرفته است.
عملیات خواندن
قبل از هر چیزی برای اینکه در مانور دادن بر روی دادهها راحت باشیم و اطلاعات را با فیلترهای متفاوتی واکشی کنیم، 7 عدد کتاب را با مشخصات زیر اضافه میکنیم. دو فیلد سال و تاریخ آخرین موجودی انبار را هم اضافه میکنیم.
var client = new MongoClient(); var db = client.GetDatabase("publisher"); db.DropCollection("books"); var collection = db.GetCollection<Book>("books"); var book =new Book() { Title = "Gone With Wind", ISBN = "43442424", Price = 50000, Year = 1936, LastStock = DateTime.Now.AddDays(-13), Language = new Language() { Name = "Persian" }, Authors = new List<Author>() { new Author() { Name = "Margaret Mitchell" }, new Author() { Name = "Ali Mahboobi (Translator)" }, } }; var book2 = new Book() { Title = "Jane Eyre", ISBN = "87897897", Price = 60000, Year = 1847, LastStock = DateTime.Now.AddDays(-5), Language = new Language() { Name = "English" }, Authors = new List<Author>() { new Author() { Name = "Charlotte Brontë" }, } }; var book3 = new Book() { Title = "White Fang", ISBN = "43442424", Price = 50000, Year = 1936, LastStock = DateTime.Now.AddDays(-13), Language = new Language() { Name = "English" }, Authors = new List<Author>() { new Author() { Name = "Jack London" }, new Author() { Name = "Philippe Mignon" }, } }; var book4 = new Book() { Title = "The Lost Symbol", ISBN = "43442424", Price = 3500000, Year = 2009, LastStock = DateTime.Now.AddDays(-17), Language = new Language() { Name = "Persian" }, Authors = new List<Author>() { new Author() { Name = "Dan Brown" }, new Author() { Name = "Mehrdad" }, } }; var book7 = new Book() { Title = "The Lost Symbol", ISBN = "43442424", Price = 47000000, Year = 2009, LastStock = DateTime.Now.AddDays(-56), Language = new Language() { Name = "Persian" }, Authors = new List<Author>() { new Author() { Name = "Dan Brown" }, new Author() { Name = "Mehrdad" }, } }; var book5= new Book() { Title = "The Help", ISBN = "45345e3er3", Price = 9000000, Year = 2009, LastStock = DateTime.Now.AddDays(-2), Language = new Language() { Name = "Enlish" }, Authors = new List<Author>() { new Author() { Name = "Kathryn Stockett" }, } }; var book6 = new Book() { Title = "City of Glass", ISBN = "454534545", Price = 500000, Year = 2009, LastStock = DateTime.Now, Language = new Language() { Name = "Persian" }, Authors = new List<Author>() { new Author() { Name = "Cassandra Clare" }, new Author() { Name = "Ali" }, } }; var books = new List<Book> {book, book2, book3, book4, book5, book6,book7}; collection.InsertManyAsync(books);
برای واکشی دیتاها کالکشنی از آن نوع را همانند قبل درخواست میکنیم. بعد از آن نیاز است که فیلتری برای واکشی اطلاعات تعریف کنیم که این فیلتر در قالب یک کلاس به نام BsonDocument ایجاد میشود که ما در اینجا، به دلیل اینکه میخواهیم همه اسناد را واکشی کنیم ، این سند Bson را مقداردهی نمیکنیم و توسط متد Find آن را در واکشی دیتاها شرکت میدهیم و سپس با صدا زدن متد ToList، عملیات واکشی را انجام میدهیم، برای اینکار میتوانیم از عملیات غیرهمزمان هم استفاده کنیم.
var client = new MongoClient(); var db = client.GetDatabase("publisher"); var collection = db.GetCollection<Book>("books"); var filter=new BsonDocument(); var docs = collection.Find(filter).ToList(); foreach (var book in docs) { Console.WriteLine(book.Title + " By "+ book.Authors[0].Name); }
با اجرای کد بالا به نتایج زیر میرسیم:
Gone With Wind By Margaret Mitchell Jane Eyre By Charlotte Brontë White Fang By Jack London The Lost Symbol By Dan Brown The Help By Kathryn Stockett City of Glass By Cassandra Clare The Lost Symbol By Dan Brown
اگر بخواهید فیلتری را بر روی این واکشی قرار دهید و مثلا بخواهید کتابهای منتشر شده در سال 2009 را واکشی نمایید، باید این سند Bson را مقداردهی نمایید. ولی برای راحتی اینکار، این کتابخانه شامل یک بیلدر Builder بوده که میتوان از طریق آن فیلترهای متنوعی را به صورت سادهتر طراحی کنید:
در خطوط بالا ابتدا یک بیلدر را برای کلاس مورد نظر ایجاد کرده و از خصوصیت Filter آن استفاده میکنیم و این خصوصیت شامل متدهای فراوانی است که میتوانید برای ایجاد شرط یا فیلتر استفاده کنید. تعدادی از متدهای پر استفاده آن همانند eq (برابری) ، gt (برزگتر از ...) ، gte (بزرگتر مساوی ...) و طبیعتا خانواده lt و ... موجود هستند.
var filter = Builders<Book>.Filter.Eq("Year", 2009); var docs = collection.Find(filter).ToList(); foreach (var book in docs) { Console.WriteLine(book.Title + " By "+ book.Authors[0].Name); }
The Lost Symbol By Dan Brown The Help By Kathryn Stockett City of Glass By Cassandra Clare The Lost Symbol By Dan Brown
// var filter=new BsonDocument(); var filterBuilder = Builders<Book>.Filter; var filter= filterBuilder.Eq("Year", 2009) | filterBuilder.Gte("Price",700000); var docs = collection.Find(filter).ToList(); foreach (var book in docs) { Console.WriteLine(book.Title + " By "+ book.Authors[0].Name); }
Gone With Wind By Margaret Mitchell White Fang By Jack London The Lost Symbol By Dan Brown The Help By Kathryn Stockett City of Glass By Cassandra Clare The Lost Symbol By Dan Brown
برای اینکه بتوانید از linq به جای queryBuilder استفاده کنید، میتوانید از خصوصیت AsQueryable استفاده کنید. خط زیر همان شرط یا فیلتر بالا را توسط Linq اعمال میکند
var docs = collection.AsQueryable().Where(x => x.Year == 2009 || x.Price <= 50000).ToList();
Sort کردن دادهها
برای مرتب سازی اطلاعات به شیوه کوئری بیلدر، همانند فیلتر که از کلاس Builder استفاده میکردیم، از همین شیء استفاده میکنیم؛ با این تفاوت که بجای استفاده از خصوصیت Filter، از Sort استفاده میکنیم و شیء ایجاد شده را به متد Sort میدهیم:
var sort = Builders<Book>.Sort.Ascending("Title").Descending("Price"); var docs = collection.Find(filter).Sort(sort).ToList(); foreach (var book in docs) { Console.WriteLine(book.Title + " By "+ book.Authors[0].Name); }
City of Glass By Cassandra Clare Gone With Wind By Margaret Mitchell The Help By Kathryn Stockett The Lost Symbol By Dan Brown The Lost Symbol By Dan Brown White Fang By Jack London
توجه داشته باشید که متد sort بعد از فیلتر گذاری، یعنی عمل Find در دسترس میباشد.
در قسمت بعدی به روزرسانی، حذف و ایندکس گذاری را مورد بررسی قرار میدهیم.
public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { }
سؤال: در زمان اجرای ScalarExecuted دقیقا در کجا قرار داریم؟ چه متدی در برنامه، در کدام کلاس، سبب رسیدن به این نقطه شدهاست؟
تمام این اطلاعات را در زمان اجرا توسط کلاس StackTrace میتوان بدست آورد:
public static string GetCallingMethodInfo() { var stackTrace = new StackTrace(true); var frameCount = stackTrace.FrameCount; var info = new StringBuilder(); var prefix = "-- "; for (var i = frameCount - 1; i >= 0; i--) { var frame = stackTrace.GetFrame(i); var methodInfo = getStackFrameInfo(frame); if (string.IsNullOrWhiteSpace(methodInfo)) continue; info.AppendLine(prefix + methodInfo); prefix = "-" + prefix; } return info.ToString(); }
خاصیت stackTrace.FrameCount مشخص میکند که در زمان فراخوانی متد GetCallingMethodInfo که اکنون برای مثال درون متد ScalarExecuted قرار گرفتهاست، از چند سطح بالاتر این فراخوانی صورت گرفتهاست. سپس با استفاده از متد stackTrace.GetFrame میتوان به اطلاعات هر سطح دسترسی یافت.
در هر StackFrame دریافتی، با فراخوانی stackFrame.GetMethod میتوان نام متد فراخوان را بدست آورد. متد stackFrame.GetFileLineNumber دقیقا شماره سطری را که فراخوانی از آن صورت گرفته، بازگشت میدهد و stackFrame.GetFileName نیز نام فایل مرتبط را مشخص میکند.
یک نکته:
شرط عمل کردن متدهای stackFrame.GetFileName و stackFrame.GetFileLineNumber در زمان اجرا، وجود فایل PDB اسمبلی در حال بررسی است. بدون آن اطلاعات محل قرارگیری فایل سورس مرتبط و شماره سطر فراخوان، قابل دریافت نخواهند بود.
اکنون بر اساس این اطلاعات، متد getStackFrameInfo چنین پیاده سازی را خواهد داشت:
private static string getStackFrameInfo(StackFrame stackFrame) { if (stackFrame == null) return string.Empty; var method = stackFrame.GetMethod(); if (method == null) return string.Empty; if (isFromCurrentAsm(method) || isMicrosoftType(method)) { return string.Empty; } var methodSignature = method.ToString(); var lineNumber = stackFrame.GetFileLineNumber(); var filePath = stackFrame.GetFileName(); var fileLine = string.Empty; if (!string.IsNullOrEmpty(filePath)) { var fileName = Path.GetFileName(filePath); fileLine = string.Format("[File={0}, Line={1}]", fileName, lineNumber); } var methodSignatureFull = string.Format("{0} {1}", methodSignature, fileLine); return methodSignatureFull; }
Void Main(System.String[]) [File=Program.cs, Line=28]
در اینجا خروجی نهایی متد GetCallingMethodInfo به شکل زیر است که در آن چند سطح فراخوانی را میتوان مشاهده کرد:
-- Void Main(System.String[]) [File=Program.cs, Line=28] --- Void disposedContext() [File=Program.cs, Line=76] ---- Void Opened(System.Data.Common.DbConnection, System.Data.Entity.Infrastructure.Interception.DbConnectionInterceptionContext) [File=DatabaseInterceptor.cs,Line=157]
جهت تعدیل خروجی متد GetCallingMethodInfo، عموما نیاز است مثلا از کلاس یا اسمبلی جاری صرفنظر کرد یا اسمبلیهای مایکروسافت نیز در این بین شاید اهمیتی نداشته باشند و بیشتر هدف بررسی سورسهای موجود است تا فراخوانیهای داخلی یک اسمبلی ثالث:
private static bool isFromCurrentAsm(MethodBase method) { return method.ReflectedType == typeof(CallingMethod); } private static bool isMicrosoftType(MethodBase method) { if (method.ReflectedType == null) return false; return method.ReflectedType.FullName.StartsWith("System.") || method.ReflectedType.FullName.StartsWith("Microsoft."); }
کد کامل CallingMethod.cs را از اینجا میتوانید دریافت کنید:
CallingMethod.cs
استفاده از NuGet Package Manager برای مدیریت وابستگیها
درباره اهمیت NuGet برای مصرف کنندگان قبلا این مطلب نوشته شده است.
بجای صرف وقت برای اینکه بررسی کنیم آیا این نسخهی جدید کتابخانهی X یا اسکریپت jQuery آمده است یا خیر، میتوان این وظیفه را به NuGet سپرد. علاوه بر این NuGet مزیتهای دیگری هم دارد؛ مثلا تیمهای برنامه نویسی میتوانند کتاب خانههای مشترک خودشان را در مخزنهای سفارشی NuGet قرار دهند و توزیع و Versioning آنرا به NuGet بسپارند.
تکیه بر Abstraction (انتزاع)
Abstraction در طراحی سیستمها منجر به تولید نرم افزار هایی Loosely coupled با قابلیت نگهداری بالا و همچنین فراهم شدن زمینه برای نوشتن Unit Test میشود.
اگر به مطالب قبلی وب سایت برگردیم در مطلب چرا ASP.NET MVC گفته شد که :
2) دستیابی به کنترل بیشتر بر روی اجزای فریم ورک :
در طراحی ASP.NET MVC همهجا interfaceها قابل مشاهد هستند. همین مساله به معنای افزونه پذیری اکثر قطعات تشکیل دهنده ASP.NET MVC است؛ برخلاف ASP.NET web forms. برای مثال تابحال چندین view engine، routing engine و غیره توسط برنامه نویسهای مستقل برای ASP.NET MVC طراحی شدهاند که هیچکدام با ASP.NET web forms میسر نیست. برای مثال از view engine پیش فرض آن خوشتان نمیآید؟ عوضش کنید! سیستم اعتبار سنجی توکار آنرا دوست ندارید؟ آنرا با یک نمونه بهتر تعویض کنید و الی آخر ...
به علاوه طراحی بر اساس interfaceها یک مزیت دیگر را هم به همراه دارد و آن هم ساده سازی mocking (تقلید) آنها است جهت ساده سازی نوشتن آزمونهای واحد.
از کلمهی کلیدی New استفاده نکنید
هر جا ممکن است کار وهله سازی اشیاء را به لایه و حتی Framework دیگری بسپارید. هر زمان اشیاء نرم افزار خودتان را با کلمهی new وهله سازی میکنید اصل Abstraction را فراموش کرده اید. هر زمان اشیاء نرم افزار را مستقیم وهله سازی میکنید در نظر داشته باشید میتوانید آنها را به صورت وابستگی تزریق کنید.
در همین رابطه مطالب زیر را از دست ندهید :
- تزریق وابستگی (dependency injection) به زبان ساده
- تزریق وابستگی (Dependency Injection) و توسعه پذیری
از HttpContext مستقیما استفاده نکنید (از HttpContextBase استفاده کنید)
از .NET 4 به بعد فضای نامی تعریف شده که در بر دارندهی کلاسهای انتزاعی (Abstraction) خیلی از قسمتهای اصلی ASP.NET است. یکی از مواردی که در توسعهی ASP.NET معمولا زیاد استفاده میشود، شیء HttpContext است . استفاده از HttpContextBase را به استفاده از HttpContext ترجیح دهید. اهمیت این موضوع در راستای اهمیت انتزاع (Abstraction) میباشد.
از "رشتههای جادویی" اجتناب کنید
استفاده از رشتههای جادویی در خیلی از جاها کار را ساده میکند؛ بعضی وقتها هم به آنها نیاز است اما مشکلات زیادی دارند :
- رشتهها معنای باطنی ندارند (مثلا : دشوار است که از روی نام یک ID مشخص کنم این ID چطور به ID دیگری مرتبط است و یا اصلا ربط دارد یا خیر)
- با اشتباهات املایی یا عدم رعایت حروف بزرگ و کوچک ایجاد مشکل میکنند.
- به Refactoring واکنش خوبی نشان نمیدهند. (برای درک بهتر این مطلب را بخوانید.)
<p> <label for="FirstName">First Name:</label> <span id="FirstName">@ViewData["FirstName"]</span> </p>
<p> <label for="FirstName">First Name:</label> <span id="FirstName">@Model.FirstName</span> </p>
در رابطه با Magic strings این مطلب را مطالعه بفرمایید.
از نوشتن HTML در کدهای "Backend" خودداری کنید
با توجه به اصل جداسازی وابستگیها (Separation of Concerns) وظیفهی کنترلرها و دیگر کدهای backend رندر کردن HTML نیست. (ساده سازی کنترلر ها) البته در نظر داشته باشید که قطعا تولید HTML در متدهای کمکی کلاس هایی که "تنها" وظیفهی آنها کمک به Viewها جهت تولید کد هست ایرادی ندارد. این کلاسها بخشی از View در نظر گرفته میشوند نه کدهای "backend".
در Viewها "Business logic" انجام ندهید
معکوس بند قبلی هم کاملا صدق میکند ، منظور این است که Viewها تا جایی که ممکن است باید حاوی کمترین Business logic ممکن باشند. در واقع تمرکز Viewها باید استفاده و نحوهی نمایش داده ای که برای آنها فراهم شده باشد نه انجام عملیات روی آن.
استفاده از Presentation Model را به استفاده مستقیم از Business Objectها ترجیح دهید
در مطالب مختلف وب سایت اشاره به اهمین ViewModelها شده است. برای اطلاعات بیشتر بند ج آموزش 11 از سری آموزشهای ASP.NET MVC را مطالعه بفرمایید.
Ifهای شرطی را در Viewها را در متدهای کمکی کپسوله کنید
استفاده از شرطها در View کار توسعه دهنده را برای یک سری اعمال ساده میکند اما میتواند باعث کمی کثیف کاری هم شود. مثلا:
@if(Model.IsAnonymousUser) { <img src="@Url.Content("~/content/images/anonymous.jpg")" /> } else if(Model.IsAdministrator) { <img src="@Url.Content("~/content/images/administrator.jpg")" /> } else if(Model.Membership == Membership.Standard) { <img src="@Url.Content("~/content/images/member.jpg")" /> } else if(Model.Membership == Membership.Preferred) { <img src="@Url.Content("~/content/images/preferred_member.jpg")" /> }
public static string UserAvatar(this HtmlHelper<User> helper) { var user = helper.ViewData.Model; string avatarFilename = "anonymous.jpg"; if (user.IsAnonymousUser) { avatarFilename = "anonymous.jpg"; } else if (user.IsAdministrator) { avatarFilename = "administrator.jpg"; } else if (user.Membership == Membership.Standard) { avatarFilename = "member.jpg"; } else if (user.Membership == Membership.Preferred) { avatarFilename = "preferred_member.jpg"; } var urlHelper = new UrlHelper(helper.ViewContext.RequestContext); var contentPath = string.Format("~/content/images/{0}", avatarFilename) string imageUrl = urlHelper.Content(contentPath); return string.Format("<img src='{0}' />", imageUrl); }
@Html.UserAvatar()
منتظر قسمت بعدی باشید.
دریافت کتابخانه DNT Scheduler و مثال آن
DNTScheduler
در این بسته، کدهای کتابخانهی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده میشود، اهمیتی ندارد که پروژهی شما وب فرم است یا MVC. با هر دو حالت کار میکند.
نحوهی تعریف یک وظیفهی جدید
کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع میشود:
public class SendEmailsTask : ScheduledTaskTemplate
using System; namespace DNTScheduler.TestWebApplication.WebTasks { public class SendEmailsTask : ScheduledTaskTemplate { /// <summary> /// اگر چند جاب در یک زمان مشخص داشتید، این خاصیت ترتیب اجرای آنها را مشخص خواهد کرد /// </summary> public override int Order { get { return 1; } } public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Minute % 2 == 0 && now.Second == 1; } public override void Run() { if (this.IsShuttingDown || this.Pause) return; System.Diagnostics.Trace.WriteLine("Running Send Emails"); } public override string Name { get { return "ارسال ایمیل"; } } } }
- متد RunAt ثانیهای یکبار فراخوانی میشود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه میخواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آنها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفهی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا میشود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Hour == 23 && now.Minute == 33 && now.Second == 1; }
خاصیت Pause هر وظیفه را برنامه میتواند تغییر دهد. به این ترتیب در مورد توقف یا ادامهی یک وظیفه میتوان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار میدهد.
- در متد Run، منطق وظیفهی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیهی بک آپ.
- Name نیز نام وظیفهی جاری است که میتواند در گزارشات مفید باشد.
همین مقدار برای تعریف یک وظیفه کافی است.
نحوهی ثبت و راه اندازی وظایف تعریف شده
پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آنها میتوان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
using System; using System.Net; namespace DNTScheduler.TestWebApplication.WebTasks { public static class ScheduledTasksRegistry { public static void Init() { ScheduledTasksCoordinator.Current.AddScheduledTasks( new SendEmailsTask(), new DoBackupTask()); ScheduledTasksCoordinator.Current.OnUnexpectedException = (exception, scheduledTask) => { //todo: log the exception. System.Diagnostics.Trace.WriteLine(scheduledTask.Name + ":" + exception.Message); }; ScheduledTasksCoordinator.Current.Start(); } public static void End() { ScheduledTasksCoordinator.Current.Dispose(); } public static void WakeUp(string pageUrl) { try { using (var client = new WebClient()) { client.Credentials = CredentialCache.DefaultNetworkCredentials; client.Headers.Add("User-Agent", "ScheduledTasks 1.0"); client.DownloadData(pageUrl); } } catch (Exception ex) { //todo: log ex System.Diagnostics.Trace.WriteLine(ex.Message); } } } }
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاسهای وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع میشود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آنرا توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException میتوانید دریافت کنید. کتابخانهی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده میکند تا در حین اجرای وظایف، برنامهی جاری با اخلال و کندی مواجه نشده و بتواند به درخواستهای رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، میتواند کل پروسهی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام میدهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه میدهد.
- از متد WakeUp تعریف شده میتوان برای بیدار کردن مجدد برنامه استفاده کرد.
استفاده از کلاس ScheduledTasksRegistry تعریف شده
پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آنرا به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
using System; using System.Configuration; using DNTScheduler.TestWebApplication.WebTasks; namespace DNTScheduler.TestWebApplication { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { ScheduledTasksRegistry.Init(); } protected void Application_End() { ScheduledTasksRegistry.End(); //نکته مهم این روش نیاز به سرویس پینگ سایت برای زنده نگه داشتن آن است ScheduledTasksRegistry.WakeUp(ConfigurationManager.AppSettings["SiteRootUrl"]); } } }
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، میتوانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواستهای رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه میتوانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
<?xml version="1.0"?> <configuration> <appSettings> <add key="SiteRootUrl" value="http://localhost:10189/Default.aspx" /> </appSettings> </configuration>
گزارشگیری از وظایف تعریف شده
برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
var jobsList = ScheduledTasksCoordinator.Current.ScheduledTasks.Select(x => new { TaskName = x.Name, LastRunTime = x.LastRun, LastRunWasSuccessful = x.IsLastRunSuccessful, IsPaused = x.Pause, }).ToList();
EF Code First #4
آشنایی با Code first migrations
ویژگی Code first migrations برای اولین بار در EF 4.3 ارائه شد و هدف آن سهولت هماهنگ سازی کلاسهای مدل برنامه با بانک اطلاعاتی است؛ به صورت خودکار یا با تنظیمات دقیق دستی.
همانطور که در قسمتهای قبل نیز به آن اشاره شد، تا پیش از EF 4.3، پنج روال جهت آغاز به کار با بانک اطلاعاتی در EF code first وجود داشت و دارد:
1) در اولین بار اجرای برنامه، در صورتیکه بانک اطلاعاتی اشاره شده در رشته اتصالی وجود خارجی نداشته باشد، نسبت به ایجاد خودکار آن اقدام میگردد. اینکار پس از وهله سازی اولین DbContext و همچنین صدور یک کوئری به بانک اطلاعاتی انجام خواهد شد.
2) DropCreateDatabaseAlways : همواره پس از شروع برنامه، ابتدا بانک اطلاعاتی را drop کرده و سپس نمونه جدیدی را ایجاد میکند.
3) DropCreateDatabaseIfModelChanges : اگر EF Code first تشخیص دهد که تعاریف مدلهای شما با بانک اطلاعاتی مشخص شده توسط رشته اتصالی، هماهنگ نیست، آنرا drop کرده و نمونه جدیدی را تولید میکند.
4) با مقدار دهی پارامتر متد System.Data.Entity.Database.SetInitializer به نال، میتوان فرآیند آغاز خودکار بانک اطلاعاتی را غیرفعال کرد. در این حالت شخص میتواند تغییرات انجام شده در کلاسهای مدل برنامه را به صورت دستی به بانک اطلاعاتی اعمال کند.
5) میتوان با پیاده سازی اینترفیس IDatabaseInitializer، یک آغاز کننده بانک اطلاعاتی سفارشی را نیز تولید کرد.
اکثر این روشها در حین توسعه یک برنامه یا خصوصا جهت سهولت انجام آزمونهای خودکار بسیار مناسب هستند، اما به درد محیط کاری نمیخورند؛ زیرا drop یک بانک اطلاعاتی به معنای از دست دادن تمام اطلاعات ثبت شده در آن است. برای رفع این مشکل مهم، مفهومی به نام «Migrations» در EF 4.3 ارائه شده است تا بتوان بانک اطلاعاتی را بدون تخریب آن، بر اساس اطلاعات تغییر کردهی کلاسهای مدل برنامه، تغییر داد. البته بدیهی است زمانیکه توسط NuGet نسبت به دریافت و نصب EF اقدام میشود، همواره آخرین نگارش پایدار که حاوی اطلاعات و فایلهای مورد نیاز جهت کار با «Migrations» است را نیز دریافت خواهیم کرد.
تنظیمات ابتدایی Code first migrations
در اینجا قصد داریم همان مثال قسمت قبل را ادامه دهیم. در آن مثال از یک نمونه سفارشی سازی شده DropCreateDatabaseAlways استفاده شد.
نیاز است از منوی Tools در ویژوال استودیو، گزینه Library package manager آن، گزینه package manager console را انتخاب کرد تا کنسول پاورشل NuGet ظاهر شود.
اطلاعات مرتبط با پاورشل EF، به صورت خودکار توسط NuGet نصب میشود. برای مثال جهت مشاهده آنها به مسیر packages\EntityFramework.4.3.1\tools در کنار پوشه پروژه خود مراجعه نمائید.
در ادامه در پایین صفحه، زمانیکه کنسول پاورشل NuGet ظاهر میشود، ابتدا باید دقت داشت که قرار است فرامین را بر روی چه پروژهای اجرا کنیم. برای مثال اگر تعاریف DbContext را به یک اسمبلی و پروژه class library مجزا انتقال دادهاید، گزینه Default project را در این قسمت باید به این پروژه مجزا، تغییر دهید.
سپس در خط فرمان پاور شل، دستور enable-migrations را وارد کرده و دکمه enter را فشار دهید.
پس از اجرای این دستور، یک سری اتفاقات رخ خواهد داد:
الف) پوشهای به نام Migrations به پروژه پیش فرض مشخص شده در کنسول پاورشل، اضافه میشود.
ب) دو کلاس جدید نیز در آن پوشه تعریف خواهند شد به نامهای Configuration.cs و یک نام خودکار مانند number_InitialCreate.cs
ج) در کنسول پاور شل، پیغام زیر ظاهر میگردد:
Detected database created with a database initializer. Scaffolded migration '201205050805256_InitialCreate'
corresponding to current database schema. To use an automatic migration instead, delete the Migrations
folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.
با توجه به اینکه در مثال قسمت سوم، از آغاز کننده سفارشی سازی شده DropCreateDatabaseAlways استفاده شده بود، اطلاعات آن در جدول سیستمی dbo.__MigrationHistory در بانک اطلاعاتی برنامه موجود است (تصویری از آنرا در قسمت اول این سری مشاهده کردید). سپس با توجه به ساختار بانک اطلاعاتی جاری، دو کلاس خودکار زیر را ایجاد کرده است:
namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
}
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class InitialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
LastName = c.String(),
Email = c.String(),
Description = c.String(),
Photo = c.Binary(),
RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
Interests_Interest1 = c.String(maxLength: 450),
Interests_Interest2 = c.String(maxLength: 450),
AddDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.Id);
CreateTable(
"Projects",
c => new
{
Id = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 50),
Description = c.String(),
RowVesrion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
AddDate = c.DateTime(nullable: false),
AdminUser_Id = c.Int(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("Users", t => t.AdminUser_Id)
.Index(t => t.AdminUser_Id);
}
public override void Down()
{
DropIndex("Projects", new[] { "AdminUser_Id" });
DropForeignKey("Projects", "AdminUser_Id", "Users");
DropTable("Projects");
DropTable("Users");
}
}
}
در این کلاس خودکار، نحوه ایجاد جداول بانک اطلاعاتی تعریف شدهاند. در متد تحریف شده Up، کار ایجاد بانک اطلاعاتی و در متد تحریف شده Down، دستورات حذف جداول و قیود ذکر شدهاند.
به علاوه اینبار متد Seed را در کلاس مشتق شده از DbMigrationsConfiguration، میتوان تحریف و مقدار دهی کرد.
علاوه بر اینها جدول سیستمی dbo.__MigrationHistory نیز با اطلاعات جاری مقدار دهی میگردد.
فعال سازی گزینههای مهاجرت خودکار
برای استفاده از این کلاسها، ابتدا به فایل Configuration.cs مراجعه کرده و خاصیت AutomaticMigrationsEnabled را true کنید:
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
پس از آن EF به صورت خودکار کار استفاده و مدیریت «Migrations» را عهدهدار خواهد شد. البته برای این منظور باید نوع آغاز کننده بانک اطلاعاتی را از DropCreateDatabaseAlways قبلی به نمونه جدید MigrateDatabaseToLatestVersion نیز تغییر دهیم:
//Database.SetInitializer(new Sample2DbInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample2Context, Migrations.Configuration>());
یک نکته:
کلاس Migrations.Configuration که باید در حین وهله سازی از MigrateDatabaseToLatestVersion قید شود (همانند کدهای فوق)، از نوع internal sealed معرفی شده است. بنابراین اگر این کلاس را در یک اسمبلی جداگانه قرار دادهاید، نیاز است فایل را ویرایش کرده و internal sealed آنرا به public تغییر دهید.
روش دیگر معرفی کلاسهای Context و Migrations.Configuration، حذف متد Database.SetInitializer و استفاده از فایل app.config یا web.config است به نحو زیر ( در اینجا حرف ` اصطلاحا back tick نام دارد. فشردن دکمه ~ در حین تایپ انگلیسی):
<entityFramework>
<contexts>
<context type="EF_Sample02.Sample2Context, EF_Sample02">
<databaseInitializer
type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EF_Sample02.Sample2Context, EF_Sample02],
[EF_Sample02.Migrations.Configuration, EF_Sample02]], EntityFramework"
/>
</context>
</contexts>
</entityFramework>
آزمودن ویژگی مهاجرت خودکار
اکنون برای آزمایش این موارد، یک خاصیت دلخواه را به کلاس Project به نام public string SomeProp اضافه کنید. سپس برنامه را اجرا نمائید.
در ادامه به بانک اطلاعاتی مراجعه کرده و فیلدهای جدول Projects را بررسی کنید:
CREATE TABLE [dbo].[Projects](
---...
[SomeProp] [nvarchar](max) NULL,
---...
بله. اینبار فیلد SomeProp بدون از دست رفتن اطلاعات و drop بانک اطلاعاتی، به جدول پروژهها اضافه شده است.
عکس العمل ویژگی مهاجرت خودکار در مقابل از دست رفتن اطلاعات
در ادامه، خاصیت public string SomeProp را که در قسمت قبل به کلاس پروژه اضافه کردیم، حذف کنید. اکنون مجددا برنامه را اجرا نمائید. برنامه بلافاصله با استثنای زیر متوقف خواهد شد:
Automatic migration was not applied because it would result in data loss.
از آنجائیکه حذف یک خاصیت مساوی است با حذف یک ستون در جدول بانک اطلاعاتی، امکان از دست رفتن اطلاعات در این بین بسیار زیاد است. بنابراین ویژگی مهاجرت خودکار دیگر اعمال نخواهد شد و این مورد به نوعی یک محافظت خودکار است که درنظر گرفته شده است.
البته در EF Code first این مساله را نیز میتوان کنترل نمود. به کلاس Configuration اضافه شده توسط پاورشل مراجعه کرده و خاصیت AutomaticMigrationDataLossAllowed را به true تنظیم کنید:
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = true;
this.AutomaticMigrationDataLossAllowed = true;
}
این تغییر به این معنا است که خودمان صریحا مجوز حذف یک ستون و اطلاعات مرتبط به آنرا صادر کردهایم.
پس از این تغییر، مجددا برنامه را اجرا کنید. ستون SomeProp به صورت خودکار حذف خواهد شد، اما اطلاعات رکوردهای موجود تغییری نخواهند کرد.
استفاده از Code first migrations بر روی یک بانک اطلاعاتی موجود
تفاوت یک دیتابیس موجود با بانک اطلاعاتی تولید شده توسط EF Code first در نبود جدول سیستمی dbo.__MigrationHistory است.
به این ترتیب زمانیکه فرمان enable-migrations را در یک پروژه EF code first متصل به بانک اطلاعاتی قدیمی موجود اجرا میکنیم، پوشه Migration در آن ایجاد خواهد شد اما تنها حاوی فایل Configuration.cs است و نه فایلی شبیه به number_InitialCreate.cs .
بنابراین نیاز است به صورت صریح به EF اعلام کنیم که نیاز است تا جدول سیستمی dbo.__MigrationHistory و فایل number_InitialCreate.cs را نیز تولید کند. برای این منظور کافی است دستور زیر را در خط فرمان پاورشل NuGet پس از فراخوانی enable-migrations اولیه، اجرا کنیم:
add-migration Initial -IgnoreChanges
با بکارگیری پارامتر IgnoreChanges، متد Up در فایل number_InitialCreate.cs تولید نخواهد شد. به این ترتیب نگران نخواهیم بود که در اولین بار اجرای برنامه، تعاریف دیتابیس موجود ممکن است اندکی تغییر کند.
سپس دستور زیر را جهت به روز رسانی جدول سیستمی dbo.__MigrationHistory اجرا کنید:
update-database
پس از آن جهت سوئیچ به مهاجرت خودکار، خاصیت AutomaticMigrationsEnabled = true را در فایل Configuration.cs همانند قبل مقدار دهی کنید.
مشاهده دستوارت SQL به روز رسانی بانک اطلاعاتی
اگر علاقمند هستید که دستورات T-SQL به روز رسانی بانک اطلاعاتی را نیز مشاهده کنید، دستور Update-Database را با پارامتر Verbose آغاز نمائید:
Update-Database -Verbose
و اگر تنها نیاز به مشاهده اسکریپت تولیدی بدون اجرای آنها بر روی بانک اطلاعاتی مدنظر است، از پارامتر Script باید استفاده کرد:
update-database -Script
نکتهای در مورد جدول سیستمی dbo.__MigrationHistory
تنها دلیلی که این جدول در SQL Server البته (ونه برای مثال در SQL Server CE) به صورت سیستمی معرفی میشود این است که «جلوی چشم نباشد»! به این ترتیب در SQL Server management studio در بین سایر جداول معمولی بانک اطلاعاتی قرار نمیگیرد. اما برای EF تفاوتی نمیکند که این جدول سیستمی است یا خیر.
همین سیستمی بودن آن ممکن است بر اساس سطح دسترسی کاربر اتصالی به بانک اطلاعاتی مساله ساز شود. برای نمونه ممکن است schema کاربر متصل dbo نباشد. همینجا است که کار به روز رسانی این جدول متوقف خواهد شد.
بنابراین اگر قصد داشتید خواص سیستمی آنرا لغو کنید، تنها کافی است دستورات T-SQL زیر را در SQL Server اجرا نمائید:
SELECT * INTO [TempMigrationHistory]
FROM [__MigrationHistory]
DROP TABLE [__MigrationHistory]
EXEC sp_rename [TempMigrationHistory], [__MigrationHistory]
ساده سازی پروسه مهاجرت خودکار
کل پروسهای را که در این قسمت مشاهده کردید، به صورت ذیل نیز میتوان خلاصه کرد:
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.IO;
namespace EF_Sample02
{
public class Configuration<T> : DbMigrationsConfiguration<T> where T : DbContext
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}
public class SimpleDbMigrations
{
public static void UpdateDatabaseSchema<T>(string SQLScriptPath = "script.sql") where T : DbContext
{
var configuration = new Configuration<T>();
var dbMigrator = new DbMigrator(configuration);
saveToFile(SQLScriptPath, dbMigrator);
dbMigrator.Update();
}
private static void saveToFile(string SQLScriptPath, DbMigrator dbMigrator)
{
if (string.IsNullOrWhiteSpace(SQLScriptPath)) return;
var scriptor = new MigratorScriptingDecorator(dbMigrator);
var script = scriptor.ScriptUpdate(sourceMigration: null, targetMigration: null);
File.WriteAllText(SQLScriptPath, script);
Console.WriteLine(script);
}
}
}
سپس برای استفاده از آن خواهیم داشت:
SimpleDbMigrations.UpdateDatabaseSchema<Sample2Context>();
در این کلاس ذخیره سازی اسکریپت تولیدی جهت به روز رسانی بانک اطلاعاتی جاری در یک فایل نیز درنظر گرفته شده است.
تا اینجا مهاجرت خودکار را بررسی کردیم. در قسمت بعدی Code-Based Migrations را ادامه خواهیم داد.
در مورد نحوه رمزنگاری فایلهای PDF به کمک روش Public-key encryption توسط iTextSharp مطلبی را پیشتر در این سایت مطالعه کردهاید.
این روش یک مشکل مهم دارد: «ارائه فایل PFX و همچنین کلمه عبور آن به کاربر نهایی»
خوب، این یعنی اینکه شما به راحتی میتونید اطلاعات را رمزگشایی کنید؛ چون همه چیز سخاوتمندانه در اختیارتان است. بنابراین ضرورت رمزنگاری آن در ابتدای امر زیر سؤال میرود.
اکنون این سؤال مطرح میشود که آیا میتوان این اطلاعات را تا حد قابل قبولی مخفی کرد؟ مثلا یک برنامه را در اختیار کاربر قرار داد که اطلاعات فایل PFX را به همراه کلمه عبور آن در سیستم نصب کند.
پاسخ:
دات نت به صورت توکار از این نوع فایلهای مجوز پشتیبانی میکند:
using System.Security.Cryptography.X509Certificates;
namespace InstallPfx
{
class Program
{
private static void InstallCertificate(string cerFileName, string password)
{
var certificate = new X509Certificate2(cerFileName, password, X509KeyStorageFlags.PersistKeySet);
var store = new X509Store(StoreName.My);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
store.Close();
}
static void Main(string[] args)
{
InstallCertificate(@"D:\forTest\file.pfx", "123456");
}
}
}
پس از اجرای کد فوق، امکان مشاهده فایلهای PDF رمزنگاری شده به کمک اطلاعات فایل file.pfx، میسر میشود.
برای مشاهده این مجوز نصب شده هم میتوان در دیالوگ Run ویندوز نوشت : certmgr.msc تا کنسول مدیریتی مجوزهای ویندوز ظاهر شود. سپس به قسمت personal certificates باید مراجعه کرد.
WebStorage
- مکانیزم ذخیره سازی:
- چند نسخه از مرورگر
- محدودیت حجمی
- session storage
- local storage
- SessionStorage
- LocalStorage
با اینکه توصیه نامه W3C از پایان کار پیاده سازی این قابلیت خبر میدهد ولی در حال حاضر که این مقاله تدوین شده است هنوز نهایی اعلام نشده است. برای پشتیبانی مرورگرهای قدیمی از webstorage میتوان از فایل جاوااسکریپتی Store.js کمک گرفت.
مفاهیم امنیتی و محافظت از داده ها
محدودیتهای حمایتی و حفاظتی webstorage دقیقا همانند کوکی هاست. به این معنی که وب سایتهای دیگر توانایی اتصال به webstorage سایت دیگری را ندارند. البته این مورد ممکن است برای وب سایت هایی که بر ساب دومین تکیه کردهاند ایجاد مشکل کند. برای حل این مسائل میتوانید از کتابخانههای سورس بازی چون Cross Storage که توسط Zendesk ارائه شده است، استفاده کرد.
همانند هر مکانیزم ذخیره سازی سمت کلاینت، مواردی توصیه میگردد که رعایت آنها از لحاظ امنیتی پر اهمیت است. به عنوان نمونه ذخیرهی اطلاعات شخصی و موارد حساس توصیه نمیگردد؛ چرا که احتمال دسترسی آسان نفوذگران به دادههای محلی و خواندن آنها وجود دارد.
Data Integrity یا یکپارچگی دادهها نیز در نظر گرفته شده است. باید حفاظتی در برابر عدم موفقیت ذخیره سازی دادهها نیز وجود داشته باشد. این عدم موفقیتها میتواند به دلایل زیر رخ دهد:
- اگر کاربر قابلیت webstorage را غیرفعال کرده باشد.
- اگر فضایی برای کاربر باقی نمانده باشد.
- با محدودیت حجمی webstorage مواجه شده است.
- با مواجه شدن با خطاها یک استثنا صادر میشود که میتوانید آن را دریافت و کنترلی را روی برنامه تحت وب داشته باشید. یک نمونه استثنا QuotaExceededError
IndexedDB
یکی از فرایندهای ذخیره سازی دادهها که همان مزایای webstorage را ارائه میدهد indexed Database API است. این قابلیت از HTML 5 اضافه شده است و قسمتی از مشخصات webstorage شناخته نمیشود. برای همین مستنداتی در حوزهی webstorage برای آن پیدا نخواهید کرد ولی قابلیتهایی فراتر از webstorage دارد.
این قابلیت پیچیدگی بیشتری را نسبت به خود webstorage ایجاد میکند، ولی فرصتهای بسیاری را برای ذخیره سازی دادههایی با معماریهای پیچیدهتر و رابطهها را میدهد. با استفاده از IndexedDB دادهها به شکل دیتابیسهای سمت سرور RDMS ذخیره میشوند و این قابلیت را دارید که به سمت آن کوئری هایی مشابه بانکهای اطلاعاتی سمت سرور را ارسال کنید.
در قسمت آتی نحوه کدنویسی آن را فرا خواهیم گرفت.
گاهی از اوقات شاید نیاز شود تا از یک کنترل Active-X در WPF استفاده شود؛ مثلا هیچ نمایش دهندهی PDF ایی را در ویندوز نمیتوان یافت که امکانات و کیفیت آن در حد Acrobat reader و Active-X آن باشد. یک روش استفاده از آنرا به کمک کنترل WebBrowser در WPF پیشتر در این سایت مطالعه کردهاید. روش معرفی شده برای WinForm هم در WPF قابل استفاده است که در ادامه شرح آن خواهد آمد.
الف) بجای اضافه کردن یک User control مخصوص WPF یک user control از نوع WinForms را به یک پروژه WPF اضافه کنید.
سپس مراحل مشابهی را مانند حالت WinForms، باید طی کرد:
ب) در VS.NET از طریق منوی Tools گزینهی Choose toolbox items ، برگهی Com components را انتخاب کنید.
ج) سپس گزینهی Adobe PDF reader را انتخاب نمائید و بر روی دکمهی OK کلیک کنید.
د) اکنون این کنترل جدید را بر روی فرم user control قسمت الف برنامه قرار دهید. به صورت خودکار COMReference های متناظر هم به پروژه اضافه میشوند.
پس از اینکه کنترل بر روی فرم قرار گرفت بهتر است به خواص آن مراجعه کرده و خاصیت Dock آنرا با Fill مقدار دهی کرد تا کنترل به صورت خودکار در هر اندازهای کل ناحیهی متناظر را پوشش دهد.
کدهای مرتبط با نمایش فایل PDF این کنترل هم به شرح زیر است:
using System.Windows.Forms;
namespace WpfPdfViewer.Controls
{
public partial class AcroReader : UserControl
{
public AcroReader(string fileName)
{
InitializeComponent();
ShowPdf(fileName);
}
public void ShowPdf(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName)) return;
axAcroPDF1.LoadFile(fileName);
axAcroPDF1.setShowToolbar(true);
axAcroPDF1.Show();
}
}
}
خوب، ما تا اینجا یک کنترل Active-X را از طریق یک User controls مخصوص WinForms به پروژهی WPF جاری اضافه کردهایم. برای اینکه بتوانیم این کنترل را درون مثلا یک User control از جنس WPF و XAML نمایش دهیم باید از کنترل WindowsFormsHost استفاده کرد. برای این منظور نیاز است تا ارجاعی را به اسمبلی WindowsFormsIntegration اضافه کنیم. پس از آن کنترل یاد شده قابل استفاده خواهد بود.
برای نمونه کدهای XAML پنجره اصلی برنامه میتواند به صورت زیر باشد:
<Window x:Class="WpfPdfViewer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<WindowsFormsHost x:Name="WindowsFormsHost1" />
</Grid>
</Window>
سپس جهت استفاده از کنترل WindowsFormsHost خواهیم داشت:
using WpfPdfViewer.Controls;
namespace WpfPdfViewer
{
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
WindowsFormsHost1.Child = new AcroReader(@"PageSummary.pdf");
}
}
}
فقط کافی است شیء Child این کنترل را با وهلهای از یوزرکنترل AcroReader اضافه شده به برنامه مقدار دهی کنیم.
سؤال: این روش زیاد MVVM friendly نیست. به عبارتی Child را نمیتوان از طریق Binding مقدار دهی کرد. آیا راهی برای آن وجود دارد؟
پاسخ: بله. روش متداول برای حل این نوع مشکلات، نوشتن یک DependencyObject و Attached property مناسب میباشد که به آنها Behaviors هم میگویند. برای مثال یک نمونه از این پیاده سازی را در ذیل مشاهده میکنید:
using System;
using System.Windows;
using System.Windows.Forms;
using System.Windows.Forms.Integration;
namespace WpfPdfViewer.Behaviors
{
public class WindowsFormsHostBehavior : DependencyObject
{
public static readonly DependencyProperty BindableChildProperty =
DependencyProperty.RegisterAttached("BindableChild",
typeof(Control),
typeof(WindowsFormsHostBehavior),
new UIPropertyMetadata(null, BindableChildPropertyChanged));
public static Control GetBindableChild(DependencyObject obj)
{
return (Control)obj.GetValue(BindableChildProperty);
}
public static void SetBindableChild(DependencyObject obj, Control value)
{
obj.SetValue(BindableChildProperty, value);
}
public static void BindableChildPropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var windowsFormsHost = o as WindowsFormsHost;
if (windowsFormsHost == null)
throw new InvalidOperationException("This behavior can only be attached to a WindowsFormsHost.");
var control = (Control)e.NewValue;
windowsFormsHost.Child = control;
}
}
}
که نهایتا برای استفاده از آن خواهیم داشت:
<WindowsFormsHost
Behaviors:WindowsFormsHostBehavior.BindableChild="{Binding ...}" />
و در ViewModel برنامه هم مانند مثال فوق، فقط کافی است یک وهله از new AcroReader به این خاصیت قابل انقیاد از نوع Control، انتساب داده شود.
یا حتی میتوان بجای نوشتن یک BindableChild، برای مثال مسیر فایل pdf را به DependencyObject تعریف شده ارسال کرد و سپس در همانجا این وهله سازی و انتسابات صورت گیرد (بجای ViewModel برنامه که اینبار فقط مسیر را تنظیم میکند).
در این مقاله قصد داریم با استفاده از جاوااسکریپت خالص، یک برنامهی ساده را با الگوی MVC انجام دهیم. این برنامه، عملیات CRUD را پیاده سازی میکند و تنها به سه فایل index.html , script.js , style.css نیاز دارد و از هیچ کتابخانه یا فریم ورک دیگری در آن استفاده نمیکنیم.
- M مخفف Model میباشد و کار مدیریت دادهها را بر عهده دارد.
- V مخفف View میباشد و وظیفهی نمایش دادهها به کاربر را بر عهده دارد.
- C مخفف Controller میباشد و پل ارتباطی بین Model و View میباشد و مدیریت درخواستها را بر عهده دارد.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>الگوی MVC در جاوااسکریپت</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="root"></div> <script src="script.js"></script> </body> </html>
*, *::before, *::after { box-sizing: border-box } html { color: #444; } #root { max-width: 450px; margin: 2rem auto; padding: 0 1rem; } form { display: flex; margin-bottom: 2rem; } [type="text"], button { display: inline-block; -webkit-appearance: none; padding: .5rem 1rem; border: 2px solid #ccc; border-radius: 4px; } button { cursor: pointer; background: #007bff; color: white; border: 2px solid #007bff; margin: 0 .5rem; } [type="text"] { width: 100%; } [type="text"]:active, [type="text"]:focus { outline: 0; border: 2px solid #007bff; } [type="checkbox"] { margin-right: 1rem; } h1 { color: #222; } ul { padding: 0; } li { display: flex; align-items: center; padding: 1rem; margin-bottom: 1rem; background: #f4f4f4; border-radius: 4px; } li span { display: inline-block; padding: .5rem; width: 250px; border-radius: 4px; border: 2px solid transparent; } li span:hover { background: rgba(179, 215, 255, 0.52); } li span:focus { outline: 0; border: 2px solid #007bff; background: rgba(179, 207, 255, 0.52) }
class Model { constructor() {} } class View { constructor() {} } class Controller { constructor(model, view) { this.model = model this.view = view } } const app = new Controller(new Model(), new View())
class Model { constructor() { // یک آرایه از اطلاعات پیش فرض this.todos = [{ id: 1, text: 'Run a marathon', complete: false }, { id: 2, text: 'Plant a garden', complete: false }, ] } // متدی برای افزودن آیتم جدید به آرایه addTodo(todoText) { const todo = { id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1, text: todoText, complete: false, } this.todos.push(todo) } // متدی برای بروزسانی آیتم مورد نظر editTodo(id, updatedText) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo ) } // انجام میدهد filter با استفاده از متد id تابعی که عملیات حذف را بوسیله فیلد deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) } // متدی که در آن مشخص میکنیم کار مد نظرانجام شده یا خیر toggleTodo(id) { this.todos = this.todos.map(todo => todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo ) } }
app.model.addTodo('Take a nap') console.log(app.model.todos)
در حال حاضر با هر بار reload شدن صفحه، فقط اطلاعات پیش فرض، درون آرایه todos قرار میگیرد؛ ولی در ادامه آن را در local storage ذخیره میکنیم.
برای ساختن قسمت View، از جاوااسکریپت استفاده میکنیم و DOM را تغییر میدهیم. البته اینکار را بدون استفاده از JSX و یا یک templating language انجام خواهیم داد. قسمتهای دیگر برنامه مانند Controller و Model نباید درگیر تغییرات DOM یا CSS یا عناصر HTML باشند و تمام این موارد توسط View هندل میشود. کد View به نحو زیر خواهد بود:
class View { constructor() {} // ایجاد یک المنت با کلاسهای استایل دلخواه createElement(tag, className) { const element = document.createElement(tag) if (className) element.classList.add(className) return element } // DOM انتخاب و گرفتن آیتمی خاص از getElement(selector) { const element = document.querySelector(selector) return element } }
سپس قسمت سازنده کلاس View را تغییر میدهیم و تمام المنتهای مورد نیاز را در آن ایجاد میکنیم:
- ارجاعی به المنتی با آیدی root
- تگ h1 برای عنوان
- یک form، input و دکمهای برای افزودن آیتمی جدید به آرایهی todos
- یک المنت ul برای نمایش آیتمهای todos
constructor() { // root ارجاعی به المنتی با آیدی this.app = this.getElement('#root') // عنوان برنامه this.title = this.createElement('h1') this.title.textContent = 'Todos' // فرم ، اینپوت ورودی و دکمه this.form = this.createElement('form') this.input = this.createElement('input') this.input.type = 'text' this.input.placeholder = 'Add todo' this.input.name = 'todo' this.submitButton = this.createElement('button') this.submitButton.textContent = 'Submit' // برای نمایش عناط آرایه یا همان لیست کارها this.todoList = this.createElement('ul', 'todo-list') // افزودن اینپوت ورودی و دکمه به فرم this.form.append(this.input, this.submitButton) // ایجاد شده است app که اینجا ارجاعی به آن بنام root اضافه کردن تمام آیتمهای بالا در المنتی با آیدی this.app.append(this.title, this.form, this.todoList) }
get _todoText() { return this.input.value } _resetInput() { this.input.value = '' }
displayTodos(todos){ //... }
متد displayTodos یک المنت ul و liهایی را به تعداد عناصر todos ایجاد میکند و آنها را نمایش میدهد. هر زمانکه تغییراتی مانند اضافه شدن، حذف و ویرایش در todos صورت گیرد، این متد دوباره فراخوانی میشود و لیست جدید را نمایش میدهد. محتوای متد dispayTodos به شکل زیر خواهد بود:
displayTodos(todos) { // حذف تمام نودها while (this.todoList.firstChild) { this.todoList.removeChild(this.todoList.firstChild) } // اگر هیچ آیتمی در آرایه نبود این پاراگراف با متن پیش فرض نمایش داده میشود if (todos.length === 0) { const p = this.createElement('p') p.textContent = 'Nothing to do! Add a task?' this.todoList.append(p) } else { // وعناصرمربوطه را ایجاد میکند liاگه درون آرایه آیتمی قرار دارد پس به ازای آن یک عنصر todos.forEach(todo => { const li = this.createElement('li') li.id = todo.id const checkbox = this.createElement('input') checkbox.type = 'checkbox' checkbox.checked = todo.complete const span = this.createElement('span') span.contentEditable = true span.classList.add('editable') if (todo.complete) { const strike = this.createElement('s') strike.textContent = todo.text span.append(strike) } else { span.textContent = todo.text } const deleteButton = this.createElement('button', 'delete') deleteButton.textContent = 'Delete' li.append(checkbox, span, deleteButton) // نود ایجاد شده به لیست اضافه میکند this.todoList.append(li) }) } // برای خطایابی و نمایش در کنسول console.log(todos) }
در نهایت قسمت Controller را که پل ارتباطی بین View و Model میباشد، کامل میکنیم. اولین تغییراتی که در کلاس Controller ایجاد میکنیم، استفاده از متد displayTodos در سازندهی این کلاس میباشد و با هر بار تغییر این متد، دوباره فراخوانی میشود:
class Controller { constructor(model, view) { this.model = model this.view = view // نمایش اطلاعات پیش فرض this.onTodoListChanged(this.model.todos) } onTodoListChanged = todos => { this.view.displayTodos(todos) } }
چهار تابعی را که در قسمت Model ایجاد نمودیم و کار ویرایش، حذف، افزودن و اتمام کار را انجام میدادند، در کلاس کنترلر آنها را هندل میکنیم و زمانیکه کاربر دکمهای را برای افزودن یا تیک حذف آیتمی، زد، تابع مربوطه توسط کنترلر در Model فراخوانی شود:
handleAddTodo = todoText => { this.model.addTodo(todoText) } handleEditTodo = (id, todoText) => { this.model.editTodo(id, todoText) } handleDeleteTodo = id => { this.model.deleteTodo(id) } handleToggleTodo = id => { this.model.toggleTodo(id) }
چون کنترلر نمیتواند بصورت مستقیم فراخوانی شود و این توابع باید درون DOM تنظیم شوند تا به ازای رخدادهایی همچون click و change، فراخوانی شوند. پس از این توابع در قسمت View استفاده میکنیم و به کلاس View، موارد زیر را اضافه میکنیم:
bindAddTodo(handler) { this.form.addEventListener('submit', event => { event.preventDefault() if (this._todoText) { handler(this._todoText) this._resetInput() } }) } bindDeleteTodo(handler) { this.todoList.addEventListener('click', event => { if (event.target.className === 'delete') { const id = parseInt(event.target.parentElement.id) handler(id) } }) } bindToggleTodo(handler) { this.todoList.addEventListener('change', event => { if (event.target.type === 'checkbox') { const id = parseInt(event.target.parentElement.id) handler(id) } }) }
برای bind کردن این متدها در کلاس Controller، کدهای زیر را اضافه میکنیم:
this.view.bindAddTodo(this.handleAddTodo) this.view.bindDeleteTodo(this.handleDeleteTodo) this.view.bindToggleTodo(this.handleToggleTodo)
برای ذخیره اطلاعات در local storage، در سازنده کلاس Model، کد زیر را اضافه میکنیم:
this.todos = JSON.parse(localStorage.getItem('todos')) || []
متد دیگری هم در کلاس Model برای بهروز رسانی مقادیر local storage قرار میدهیم:
_commit(todos) { this.onTodoListChanged(todos) localStorage.setItem('todos', JSON.stringify(todos)) }
متدی هم برای تغییراتی که هر زمان بر روی todos اتفاق میافتد، فراخوانی شود:
deleteTodo(id) { this.todos = this.todos.filter(todo => todo.id !== id) this._commit(this.todos) }
این مقاله صرفا جهت آشنایی و نمونه کدی از پیاده سازی الگوی MVC در جاوااسکریپت میباشد.