ترجمه و تالیف: بهروز راد
وضعیت: در حال نگارش
پیشتر، آقای نصیری در بخشی از مباحث مربوط به Code First در مورد روشهای مختلف ارث بری در EF و در روش Code First صحبت کرده اند. در این مقالهی دو قسمتی، در مورد دو تا از این روشها در حالت Database First میخوانید.
چرا باید از ارث بری استفاده کنیم؟
یکی از اهداف اصلی ORMها این است که با ایجاد یک مدل مفهومی از پایگاه داده، آن را هر چه بیشتر به طرز تفکر ما از مدل شی گرای برنامه مان نزدیکتر کنند. از آنجا که ما توسعه گران از مفاهیم شی گرایی مانند "ارث بری" در کدهای خود استفاده میکنیم، نیاز داریم تا این مفهوم را در سطح پایگاه داده نیز داشته باشیم. آیا این کار امکان پذیر است؟ EF چه امکاناتی برای رسیدن به این هدف برای ما فراهم کرده است؟ در این قسمت به این سوال پاسخ خواهیم داد.
ارث بری جداول مفهومی است که در EF به راحتی قابل پیاده سازی است. سه روش برای پیاده سازی این مفهوم در مدل وجود دارد.
- Table Per Type یا TPT: خصیصههای مشترک در جدول پایه قرار دارند و به ازای هر زیر مجموعه نیز یک جدول جدا ایجاد میشود.
- Table Per Hierarchy یا TPH: تمامی خصیصهها در یک جدول وجود دارند.
- Table Per Concrete Type یا TPC: جدول پایه ای وجود ندارد و به ازای هر موجودیت دقیقاً یک جدول همراه با خصیصههای موجودیت در آن ایجاد میشود.
روش TPT
در این روش، خصیصههای مشترک در یک جدول پایه قرار دارند و به ازای هر زیر مجموعه از جدول پایه، یک جدول با خصیصههای منحصر به آن نوع ایجاد میشود. ابتدا جداول و ارتباطات بین آنها که در توضیح مثال برای این روش با آنها کار میکنیم را ببینیم.
فرض کنید قصد داریم تا در هنگام ثبت مشخصات یک دانش آموز، مقطع تحصیلی او نیز حتماً ذخیره شود. در این حالت، فیلدی با نام Degree ایجاد و تیک گزینهی Allow Nulls را از روبروی آن بر میداریم. با این حال اگر مشخصات دانش آموزان را در جدولی عمومی مثلاً با نام People ذخیره کنیم و این جدول را مکانی برای ذخیرهی مشخصات افراد دیگری مانند مدیران و معلمان نیز در نظر بگیریم، از آنجا که قصد ثبت مقطع تحصیلی برای مدیران و معلمان را نداریم، وجود فیلد Degree در کار ما اختلال ایجاد میکند. اما با ذخیرهی اطلاعات مدیران و معلمان در جداول مختص به خود، میتوان قانون غیر قابل Null بودن فیلد Degree برای دانش آموزان را به راحتی پیاده سازی کرد.
همان طور که در شکل قبل نیز مشخص است، ما یک جدول پایه با نام Persons ایجاد کرده ایم و خصیصههای مشترک بین زیر مجموعهها (FirstName و LastName) را در آن قرار داده ایم. سه موجودیت (Student، Admin و Instructor) از Persons ارث میبرند و موجودیت BusinessStudent نیز از Student ارث میبَرَد.
جداول ایجاد شده، پس از ایجاد مدل به روش Database First، به شکل زیر تبدیل میشوند.
از آنجا که قصد داریم ارتباطات ارث بری شده ایجاد کنیم، باید ارتباطات پیش فرض شکل گرفته بین موجودیتها را حذف کنیم. بدین منظور، بر روی هر خط ارتباطی در EDM Designer کلیک راست و گزینهی Delete from Model را انتخاب کنید. سپس بر روی موجودیت Person، کلیک راست کرده و از منوی Add New، گزینهی Inheritance را انتخاب کنید (شکل زیر).
شکل زیر ظاهر میشود.
قسمت بالا، موجودیت پایه، و قسمت پایین، موجودیت مشتق شده را مشخص میکند. این کار را سه مرتبه برای ایجاد ارتباط ارث بری شده بین موجودیت Person به عنوان موجودیت پایه و موجودیتهای Student، Instructor و Admin به عنوان موجودیتهای مشتق شده ایجاد کنید. همچنین یک ارتباط نیز بین موجودیت Student به عنوان موجودیت پایه و موجودیت BusinessStudent به عنوان موجودیت مشتق شده ایجاد کنید. نتیجهی کار را در شکل زیر ملاحظه میکنید.
اگر بر روی دکمهی Save در نوار ابزار Visual Studio کلیک کنید، چهار خطا در پنجرهی Error List نمایش داده میشود
این خطاها بیانگر این هستند که خصیصهی PersonId به دلیل اینکه در موجودیت پایهی Person تعریف شده است، نباید در موجودیتهای مشتق شده از آن نیز وجود داشته باشد چون موجودیتهای مشتق شده، خصیصهی PersonId را به ارث برده اند. وجود این خصیصه در زمان طراحی جدول در مدل فیزیکی الزامی بوده است اما اکنون ما با مدل مفهومی و قوانین شی گرایی سر و کار داریم. بنابراین خصیصهی PersonId را از موجودیتهای Student، Instructor، Admin و BusinessStudent حذف کنید. شکل زیر، نتیجهی کار را نشان میدهد.
اکنون اگر بر روی دکمهی Save کلیک کنید، خطاها از بین میروند.
ما خصیصهی PersonId را از موجودیتهای مشتق شده به این دلیل که آن را از موجودیت پایه ارث میبرند حذف کردیم. حال این خصیصه برای موجودیتهای مشتق شده وجود دارد اما باید مشخص کنیم که به کدام خصیصه از کلاس پایه تناظر دارد. شاید انتظار این باشد که EF، خود تشخیص بدهد که PersonId در موجودیتهای مشتق شده باید به PersonId کلاس پایهی خود تناظر داشته باشد اما در حال حاضر این کاری است که خود باید انجام دهیم. بدین منظور، بر روی هر یک از موجودیتهای مشتق شده کلیک راست کرده و گزینهی Table Mapping را انتخاب کنید. سپس همان طور که در شکل زیر مشاهده میکنید، تناظر را ایجاد کنید.
مدل ما آماده است. آن را امتحان میکنیم. در زیر، یک کوئری LINQ ساده بر روی مدل ایجاد شده را ملاحظه میکنید.
using (PersonDbEntities context = new PersonDbEntities()) { var people = from p in context.Persons select p; foreach (Person person in people) { Console.WriteLine("{0}, {1}", person.LastName, person.FirstName); } Console.ReadLine(); }
قضیه به همین جا ختم نمیشود! ما الان یک مدل ارث بری شده داریم. بهتر است مزایای آن را در عمل ببینیم. شاید دوست داشته باشیم تا فقط اطلاعات زیر مجموعهی BusinessStudent را بازیابی کنیم.
using (PersonDbEntities context = new PersonDbEntities()) { var students = from p in context.Persons.OfType<BusinessStudent>() select p; foreach (BusinessStudent student in students) { Console.WriteLine("{0}, {1}: Degree {2}, Discipline {3}", student.LastName, student.FirstName, student.Degree, student.Discipline); } Console.ReadLine(); }
همان طور که در کدهای قبل نیز مشخص است، خصیصههای LastName و FirstName از موجودیت پایه یعنی Person، خصیصهی Degree از موجودیت مشتق شدهی Student (که البته در نقش موجودیت پایه برای BusinessStudent است) و Discipline از موجودیت مشتق شده یعنی BusinessStudent خوانده میشوند.
یک روش دیگر نیز برای کار با این سلسه مراتب ارث بری وجود دارد. کوئری اول را دست نزنیم (اطلاعات موجودیت پایه را بازیابی کنیم) و پیش از انجام عملیاتی خاص، نوع موجودیت مشتق شده را بررسی کنیم. مثالی در این زمینه:
using (PersonDbEntities context = new PersonDbEntities()) { var people = from p in context.Persons select p; foreach (Person person in people) { Console.WriteLine("{0}, {1}", person.LastName, person.FirstName); if (person is Student) Console.WriteLine(" Degree: {0}", ((Student)person).Degree); if (person is BusinessStudent) Console.WriteLine(" Discipline: {0}", ((BusinessStudent)person).Discipline); } Console.ReadLine(); }
مزایای روش TPT
- امکان نرمال سازی سطح 3 در این روش به خوبی وجود دارد
- افزونگی در جداول وجود ندارد.
- اصلاح مدل آسان است (برای اضافه یا حذف کردن یک موجودیت به/از مدل فقط کافی است تا جدول متناظر با آن را از پایگاه داده حذف کنید)
- سرعت عملیات CRUD (ایجاد، بازیابی، آپدیت، حذف) دادهها با افزایش تعداد موجودیتهای شرکت کننده در سلسله مراتب ارث بری کاهش مییابد. به عنوان مثال، کوئریهای SELECT، حاوی عبارتهای JOIN خواهند بود و عدم توجه صحیح به کوئری نوشته شده میتواند منجر به حضور چندین عبارت JOIN که برای ارتباط بین جداول به کار میرود در اسکریپت تولیدی و کاهش زمان اجرای بازیابی دادهها شود.
- تعداد جداول در پایگاه داده زیاد میشود
در قسمت بعد با روش TPH آشنا میشوید.
بنابراین برای ایجاد یک کامپوننت میتوانیم به اینصورت عمل کنیم:
var app = angular.module("dntModule", []); app.component("pmApp", { template: `Hello this is a simple component` });
همانطور که مشاهده میکنید تابع component دو پارامتر را از ورودی دریافت خواهد کرد؛ نام کامپوننت و یک شیء برای تعیین تنظیمات کامپوننت. نام کامپوننت در اینجا به صورت camel case تعریف شده است؛ که در واقع یک convention برای Angular است. در اینحالت برای استفادهی از کامپوننت باید به اینصورت عمل کنیم:
<pm-app></pm-app>
در قسمت تنظیمات کامپوننت، در سادهترین حالت یک template تعیین شدهاست که بیانگر نحوهی رندر شدن یک کامپوننت میباشد. در اینحالت وقتی انگیولار به تگ فوق برسد، یک کامپوننت با نام pmApp را بارگذاری خواهد کرد.
ایجاد یک کامپوننت ساده
در ادامه میخواهیم یک کامپوننت ساده را جهت نمایش یکسری URL درون صفحه طراحی کنیم. ساختار صفحه index.html به صورت زیر خواهد بود:
<html ng-app="DNT"> <head> <meta charset="UTF-8"> <title>Using Angular Component</title> <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css"> <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css"> </head> <body> <div class="container"> <div class="row"> <div class="col-md-3"> <dnt-widget></dnt-widget> </div> </div> </div> <script src="bower_components/angular/angular.js"></script> <script src="scripts/app.js"></script> <script src="scripts/components/dnt-widget.component.js"></script> </body> </html>
در اینجا ابتدا توسط دایرکتیو ng-app، به Angular، ماژولمان را معرفی کردهایم. سپس مداخل بوتاسترپ و کتابخانهی font-awesome را مشاهده میکنید. در ادامه، کتابخانهی Angular و همچنین فایل app.js جهت معرفی ماژول برنامه معرفی شدهاست. در نهایت نیز یک فایل در مسیر ذکر شده برای قرار دادن کدهای کامپوننت در مسیر scripts/components اضافه شدهاست.
همانطور که ملاحظه میکنید، کامپوننتمان به صورت یک تگ سفارشی، درون صفحه قرار گرفته است:
<dnt-archive></dnt-archive>
در ادامه باید به Angular، نحوهی تعریف این کامپوننت را اعلام کنیم. بنابراین یک فایل جاوا اسکریپتی را با نام dnt-widget.component، با محتویات زیر ایجاد کنید:
(function () { "use strict"; var app = angular.module("DNT"); function DntArchiveController() { var model = this; model.panel = { title: "Panel Title", items: [ { title: "Dotnettips", url: "https://www.dntips.ir" }, { title: "Google", url: "http://www.google.con" }, { title: "Yahoo", url: "http://www.yahoo.con" } ] }; }; app.component("dntWidget", { templateUrl: '/scripts/components/dnt-widget.component.html', controllerAs: "model", controller: DntArchiveController }); } ());
توضیح کدهای فوق:
همانطور که مشاهده میکنید، برای پارمتر دوم کامپوننت، سه پراپرتی را تعیین کردهایم:
templateUrl: به کمک این پراپرتی به Angular گفتهایم که محتوای قالب این کامپوننت، درون یک فایل HTML مجزا قرار دارد و به صورت linked template میباشد.
controllerAs: یکی از مزایای استفاده از کامپوننتها، استفاده از controller as syntax میباشد. لازم به ذکر است اگر این پراپرتی را مقداردهی نکنیم، به صورت پیشفرض مقدار ctrl$ در نظر گرفته خواهد شد.
controller: مزیت دیگر کامپوننتها، استفاده از کنترلرها است. با استفاده از این پراپرتی، یک کنترلر را برای کامپوننتمان رجیستر کردهایم. در نتیجه زمانیکهی Angular میخواهد کامپوننتمان را نمایش دهد، تابع تعریف شده برای این پراپرتی، جهت ایجاد یک controller instance فراخوانی خواهد شد. بنابراین هر پراپرتی یا تابعی که برای این controller instance تعریف کنیم، به راحتی درون ویوی آن جهت اعمال بایندینگ در دسترس خواهد بود (در نتیجه نیازی به scope$ نخواهد بود).
درون کنترلر نیز برای راحتی کار و همچنین به عنوان یک best practice، مقدار this را توسط یک متغیر با نام model، کپچر کردهایم. در اینجا یک شیء را با نام panel نیز به مدل اضافه کردهایم.
محتویات تمپلیت:
<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> <span class="fa fa-archive"></span> {{ model.panel.title}} </h3> </div> <ul class="list-group"> <li class="list-group-item" ng-repeat="item in model.panel.items"> <span class="fa fa-industry"></span> <a href="{{ item.url }}">{{ item.title }}</a> </li> </ul> </div>
ویوی کامپوننت پیچیدگی خاصی ندارد. همانطور که مشاهده میکنید یک پنل بوتاسترپی را ایجاد کردهایم که مقدار عنوان آن و همچنین آیتمهای آن، از شیء اتچ شده به مدل دریافت خواهند شد. بنابراین اکنون اگر برنامه را اجرا کنید، خروجی کامپوننت را به اینصورت مشاهده خواهید کرد:
همانطور که مشاهده میکنید استفاده از کامپوننتها در Angular 1.5 در مقایسه با ایجاد دایرکتیوها و کنترلرها خیلی سادهتر است. در واقع امکانات این API جدید تنها به مثال فوق ختم نمیشود؛ بلکه این API یک سیستم مسیریابی جدید را نیز معرفی کرده است که در قسمتهای بعدی به آن نیز خواهیم پرداخت.
جهت تکمیل بحث نیز یک تقویم شمسی ساده را در اینجا قرار دادهام. میتوانید جهت مرور بحث جاری به کدهای آن مراجعه کنید. البته هدف از تعریف این پروژه تنها یک مثال ساده برای معرفی کامپوننتها بود و طبیعتاً باگهای زیادی دارد. اگر مایل بودید میتوانید در توسعهی آن مشارکت نمائید.
کدهای این قسمت را نیز از اینجا میتوانید دریافت کنید.
خیر
قبلا بله ، ولی به دلیل عدم تمرکز روی کار آن را ترک کرده ام .
گاهی از اوقات
- مثلا زمانیکه با ORMها کار میکنید استفاده از فیلدهای Identity در حین ثبت تعداد بالایی از رکوردها مشکل ساز میشوند. چون این فیلدها تحت کنترل دیتابیس هستند و نه برنامه، ORM نیاز دارد پس از هربار Insert یکبار آخرین Id را از بانک اطلاعاتی واکشی کند. همین مساله یعنی افت سرعت در تعداد بالای Insertها (چون یکبار کوئری Insert باید ارسال شود و یکبار هم یک Select اضافی دوم برای دریافت Id تولیدی توسط دیتابیس).
- روش دوم تعیین PK استفاده از نوع Guid است. در این حالت، هم مشکل حذف رکوردها و خالی شدن یک شماره را در این بین ندارید و هم چون عموما تحت کنترل برنامه است، سرعت کار کردن با آن بالاتر است. فقط تنها مشکل آن زیبا نبودنش است در مقایسه با یک عدد ساده فیلدهای Identity.
در مورد فیلدهای Identity، تغییر شماره Id به صلاح نیست چون:
الف) همانطور که عنوان کردید روابط بین جداول را به هم خواهد ریخت.
ب) در یک وب سایت و یا هر برنامهای، کلا آدرسها و ارجاعات قدیمی را از بین میبرد. مثلا فرض کنید شماره این مطلب 1381 است و شما آنرا یادداشت کردهاید. در روزی بعد، برنامه نویس شماره Idها را کلا ریست کرده. در نتیجه یک هفته بعد شما به شماره 1381 ایی خواهید رسید که تطابقی با مطلب مدنظر شما ندارد (حالا فرض کنید که این عدد شماره پرونده یک شخص بوده یا شماره کاربری او و نتایج و خسارات حاصل را درنظر بگیرید).
ج) این خوب است که در بین اطلاعات یک ردیف خالی وجود دارد. چون بر این اساس میتوان بررسی کرد که آیا واقعا رکوردی حذف شده یا خیر. گاهی از اوقات کاربران ادعا میکنند که اطلاعات ارسالی آنها نیست در حالیکه نبود این رکوردها به دلیل حذف بوده و نه عدم ثبت آنها. با بررسی این Idها میشود با کاربران در این مورد بحث کرد و پاسخ مناسبی را ارائه داد.
و اگر شمارهای که به کاربر نمایش میدهید فقط یک شماره ردیف است (و از این لحاظ میخواهید که حتما پشت سرهم باشد)، بهتر است یک View جدید ایجاد کنید تا این Id خود افزاینده را تولید کند (بدون استفاده از pk جدول).
پ.ن.
هدف من از این توضیحات صرفا عنوان این بود که به PK به شکل یک فیلد read only نگاه کنید. این دقیقا برخوردی است که Entity framework با این مفهوم دارد و صحیح است و اصولی. اگر در یک کشور هر روزه عدهای به رحمت ایزدی میروند به این معنا نیست که سازمان ثبت احوال باید شماره شناسنامهها را هر ماه ریست کند!
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1#
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#
قبل از شروع توضیحات متدهای کلاس Shape در ادامه پستهای قبل در ^ و ^ ابتدا به تشریح یک تصویر میپردازیم.
- در این حالت StartPoint.X < EndPoint.X و StartPoint.Y < EndPoint.Y خواهد بود. (StartPoint نقطه ای است که ابتدا ماوس شروع به ترسیم میکند، و EndPoint زمانی است که ماوس رها شده و پایان ترسیم را مشخص میکند.)
- در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
- در این حالت StartPoint.X > EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
- در این حالت StartPoint.X < EndPoint.X و StartPoint.Y > EndPoint.Y خواهد بود.
ابتدا یک کلاس کمکی به صورت استاتیک تعریف میکنیم که متدی جهت پیش نمایش رسم شی در حالت جابجایی ، رسم، و تغییر اندازه دارد.
using System; using System.Drawing; namespace PWS.ObjectOrientedPaint.Models { /// <summary> /// Helpers /// </summary> public static class Helpers { /// <summary> /// Draws the preview. /// </summary> /// <param name="g">The g.</param> /// <param name="startPoint">The start point.</param> /// <param name="endPoint">The end point.</param> /// <param name="foreColor">Color of the fore.</param> /// <param name="thickness">The thickness.</param> /// <param name="isFill">if set to <c>true</c> [is fill].</param> /// <param name="backgroundBrush">The background brush.</param> /// <param name="shapeType">Type of the shape.</param> public static void DrawPreview(Graphics g, PointF startPoint, PointF endPoint, Color foreColor, byte thickness, bool isFill, Brush backgroundBrush, ShapeType shapeType) { float x = 0, y = 0; float width = Math.Abs(endPoint.X - startPoint.X); float height = Math.Abs(endPoint.Y - startPoint.Y); if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y) { x = startPoint.X; y = startPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y) { x = endPoint.X; y = endPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y) { x = endPoint.X; y = startPoint.Y; } else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y) { x = startPoint.X; y = endPoint.Y; } switch (shapeType) { case ShapeType.Ellipse: if (isFill) g.FillEllipse(backgroundBrush, x, y, width, height); //else g.DrawEllipse(new Pen(foreColor, thickness), x, y, width, height); break; case ShapeType.Rectangle: if (isFill) g.FillRectangle(backgroundBrush, x, y, width, height); //else g.DrawRectangle(new Pen(foreColor, thickness), x, y, width, height); break; case ShapeType.Circle: float raduis = Math.Max(width, height); if (isFill) g.FillEllipse(backgroundBrush, x, y, raduis, raduis); //else g.DrawEllipse(new Pen(foreColor, thickness), x, y, raduis, raduis); break; case ShapeType.Square: float side = Math.Max(width, height); if (isFill) g.FillRectangle(backgroundBrush, x, y, side, side); //else g.DrawRectangle(new Pen(foreColor, thickness), x, y, side, side); break; case ShapeType.Line: g.DrawLine(new Pen(foreColor, thickness), startPoint, endPoint); break; case ShapeType.Diamond: var points = new PointF[4]; points[0] = new PointF(x + width / 2, y); points[1] = new PointF(x + width, y + height / 2); points[2] = new PointF(x + width / 2, y + height); points[3] = new PointF(x, y + height / 2); if (isFill) g.FillPolygon(backgroundBrush, points); //else g.DrawPolygon(new Pen(foreColor, thickness), points); break; case ShapeType.Triangle: var tPoints = new PointF[3]; tPoints[0] = new PointF(x + width / 2, y); tPoints[1] = new PointF(x + width, y + height); tPoints[2] = new PointF(x, y + height); if (isFill) g.FillPolygon(backgroundBrush, tPoints); //else g.DrawPolygon(new Pen(foreColor, thickness), tPoints); break; } if (shapeType != ShapeType.Line) { g.DrawString(String.Format("({0},{1})", x, y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x - 20, y - 25); g.DrawString(String.Format("({0},{1})", x + width, y + height), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), x + width - 20, y + height + 5); } else { g.DrawString(String.Format("({0},{1})", startPoint.X, startPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), startPoint.X - 20, startPoint.Y - 25); g.DrawString(String.Format("({0},{1})", endPoint.X, endPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(foreColor), endPoint.X - 20, endPoint.Y + 5); } } } }
- DrawPreview : این متد پیش نمایشی برای شی در زمان ترسیم، جابجایی و تغییر اندازه آماده میکند، پارامترهای آن
عبارتند از : بوم گرافیکی، نقطه شروع، نقطه پایان و رنگ قلم ترسیم پیش نمایش شی، ضخامت خط، آیا شی توپر باشد؟، الگوی پر کردن پس زمینه شی ، و نوع شی ترسیمی میباشد.
using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Net; namespace PWS.ObjectOrientedPaint.Models { /// <summary> /// Shape (Base Class) /// </summary> public abstract partial class Shape { #region Constructors (2) /// <summary> /// Initializes a new instance of the <see cref="Shape" /> class. /// </summary> /// <param name="startPoint">The start point.</param> /// <param name="endPoint">The end point.</param> /// <param name="zIndex">Index of the z.</param> /// <param name="foreColor">Color of the fore.</param> /// <param name="thickness">The thickness.</param> /// <param name="isFill">if set to <c>true</c> [is fill].</param> /// <param name="backgroundColor">Color of the background.</param> protected Shape(PointF startPoint, PointF endPoint, int zIndex, Color foreColor, byte thickness, bool isFill, Color backgroundColor) { CalulateLocationAndSize(startPoint, endPoint); Zindex = zIndex; ForeColor = foreColor; Thickness = thickness; IsFill = isFill; BackgroundColor = backgroundColor; } /// <summary> /// Initializes a new instance of the <see cref="Shape" /> class. /// </summary> protected Shape() { } #endregion Constructors #region Methods (10) // Public Methods (9) /// <summary> /// Draws the specified g. /// </summary> /// <param name="g">The g.</param> public virtual void Draw(Graphics g) { if (!IsSelected) return; float diff = Thickness + 4; Color myColor = Color.DarkSeaGreen; g.DrawString(String.Format("({0},{1})", StartPoint.X, StartPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), StartPoint.X - 20, StartPoint.Y - 25); g.DrawString(String.Format("({0},{1})", EndPoint.X, EndPoint.Y), new Font(new FontFamily("Tahoma"), 10), new SolidBrush(myColor), EndPoint.X - 20, EndPoint.Y + 5); if (ShapeType != ShapeType.Line) { g.DrawRectangle(new Pen(myColor), X, Y, Width, Height); // 1 2 3 // 8 4 // 7 6 5 var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2); var point2 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, StartPoint.Y - diff / 2); var point3 = new PointF(EndPoint.X - diff / 2, StartPoint.Y - diff / 2); var point4 = new PointF(EndPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2); var point5 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2); var point6 = new PointF((StartPoint.X - diff / 2 + EndPoint.X) / 2, EndPoint.Y - diff / 2); var point7 = new PointF(StartPoint.X - diff / 2, EndPoint.Y - diff / 2); var point8 = new PointF(StartPoint.X - diff / 2, (EndPoint.Y + StartPoint.Y) / 2 - diff / 2); g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point3.X, point3.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point4.X, point4.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point5.X, point5.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point6.X, point6.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point7.X, point7.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point8.X, point8.Y, diff, diff); } else { var point1 = new PointF(StartPoint.X - diff / 2, StartPoint.Y - diff / 2); var point2 = new PointF(EndPoint.X - diff / 2, EndPoint.Y - diff / 2); g.FillRectangle(new SolidBrush(myColor), point1.X, point1.Y, diff, diff); g.FillRectangle(new SolidBrush(myColor), point2.X, point2.Y, diff, diff); } } /// <summary> /// Points the in sahpe. /// </summary> /// <param name="point">The point.</param> /// <param name="tolerance">The tolerance.</param> /// <returns> /// <c>true</c> if [has point in sahpe] [the specified point]; otherwise, <c>false</c>. /// </returns> public virtual bool HasPointInSahpe(PointF point, byte tolerance = 5) { return point.X > (StartPoint.X - tolerance) && point.X < (EndPoint.X + tolerance) && point.Y > (StartPoint.Y - tolerance) && point.Y < (EndPoint.Y + tolerance); } /// <summary> /// Moves the specified location. /// </summary> /// <param name="location">The location.</param> /// <returns></returns> public virtual PointF Move(Point location) { StartPoint = new PointF(location.X, location.Y); EndPoint = new PointF(location.X + Width, location.Y + Height); return StartPoint; } /// <summary> /// Moves the specified dx. /// </summary> /// <param name="dx">The dx.</param> /// <param name="dy">The dy.</param> /// <returns></returns> public virtual PointF Move(int dx, int dy) { StartPoint = new PointF(StartPoint.X + dx, StartPoint.Y + dy); EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy); return StartPoint; } /// <summary> /// Resizes the specified dx. /// </summary> /// <param name="dx">The dx.</param> /// <param name="dy">The dy.</param> /// <returns></returns> public virtual SizeF Resize(int dx, int dy) { EndPoint = new PointF(EndPoint.X + dx, EndPoint.Y + dy); return new SizeF(Width, Height); } /// <summary> /// Resizes the specified start point. /// </summary> /// <param name="startPoint">The start point.</param> /// <param name="currentPoint">The current point.</param> public virtual void Resize(PointF startPoint, PointF currentPoint) { var dx = (int)(currentPoint.X - startPoint.X); var dy = (int)(currentPoint.Y - startPoint.Y); if (startPoint.X >= X - 5 && startPoint.X <= X + 5) { StartPoint = new PointF(currentPoint.X, StartPoint.Y); if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square) { Height = Width; } } else if (startPoint.X >= EndPoint.X - 5 && startPoint.X <= EndPoint.X + 5) { Width += dx; if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square) { Height = Width; } } else if (startPoint.Y >= Y - 5 && startPoint.Y <= Y + 5) { Y = currentPoint.Y; if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square) { Width = Height; } } else if (startPoint.Y >= EndPoint.Y - 5 && startPoint.Y <= EndPoint.Y + 5) { Height += dy; if (ShapeType == ShapeType.Circle || ShapeType == ShapeType.Square) { Width = Height; } } } /// <summary> /// Sets the background brush as hatch. /// </summary> /// <param name="hatchStyle">The hatch style.</param> public virtual void SetBackgroundBrushAsHatch(HatchStyle hatchStyle) { var brush = new HatchBrush(hatchStyle, BackgroundColor); BackgroundBrush = brush; } /// <summary> /// Sets the background brush as linear gradient. /// </summary> public virtual void SetBackgroundBrushAsLinearGradient() { var brush = new LinearGradientBrush(StartPoint, EndPoint, ForeColor, BackgroundColor); BackgroundBrush = brush; } /// <summary> /// Sets the background brush as solid. /// </summary> public virtual void SetBackgroundBrushAsSolid() { var brush = new SolidBrush(BackgroundColor); BackgroundBrush = brush; } // Private Methods (1) /// <summary> /// Calulates the size of the location and. /// </summary> /// <param name="startPoint">The start point.</param> /// <param name="endPoint">The end point.</param> private void CalulateLocationAndSize(PointF startPoint, PointF endPoint) { float x = 0, y = 0; float width = Math.Abs(endPoint.X - startPoint.X); float height = Math.Abs(endPoint.Y - startPoint.Y); if (startPoint.X <= endPoint.X && startPoint.Y <= endPoint.Y) { x = startPoint.X; y = startPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y >= endPoint.Y) { x = endPoint.X; y = endPoint.Y; } else if (startPoint.X >= endPoint.X && startPoint.Y <= endPoint.Y) { x = endPoint.X; y = startPoint.Y; } else if (startPoint.X <= endPoint.X && startPoint.Y >= endPoint.Y) { x = startPoint.X; y = endPoint.Y; } StartPoint = new PointF(x, y); EndPoint = new PointF(X + width, Y + height); } #endregion Methods } }
حال به تشریح سازنده کلاس میپردازیم:
- Shape: پارامترهای این سازنده به ترتیب عبارتند از نقطه شروع، نقطه پایان، عمق شی، رنگ قلم، ضخامت خط، آیا شی توپر باشد؟، و رنگ پر کردن شی، در این سازنده ابتدا توسط متدی به نام CalulateLocationAndSize(startPoint, endPoint); b نقاط ابتدا و انتهای شی مورد نظر تنظیم میشود، در متد مذکور بررسی میشود در صورتی که نقاط شروع و پایان یکی از حالتهای 1 ، 2، 3، 4 از تصویر ابتدا پست باشد همگی تبدیل به حالت 1 خواهد شد.
سپس به تشریح متدهای کلاس Shape میپردازیم:
- Draw: این متد دارای یک پارامتر ورودی است که بوم گرافیکی مورد نظر میباشد، در واقع شی مورد نظر خود را بروی این بوم گرافیکی ترسیم میکند. در کلاس پایه کار این متد زیاد پیچیده نیست، در صورتی که شی در حالت انتخاب باشد (IsSelected = true) بروی شی مورد نظر 8 مربع کوچک ترسیم میشود و اگر شی مورد نظر خط باشد دو مربع کوچک در طرفین خط رسم میشود که نشان دهنده انتخاب شدن شی مورد نظر است. این متد به صورت virtual تعریف شده است یعنی کلاس هایی که از Shape ارث میبرند میتوانند این متد را برای خود از نو بازنویسی کرده (override کنند) و تغییر رفتار دهند.
- HasPointInSahpe : این متد نیز به صورت virtual تعریف شده است دارای خروجی بولین میباشد. پارامترهای این متد عبارتند از یک نقطه و یک عدد که نشان دهنده تلرانش نقطه بر حسب پیکسل میباشد. کار این متد این است که یک نقطه را گرفته و بررسی میکند که آیا نقطه مورد نظر با تلرانس وارد شده آیا در داخل شی واقع شده است یا خیر (مثلا وجود نقطه در مستطیل یا وجود نقطه در دایره فرمولهای متفاوتی دارند که در اینجا پیش فرض برای تمامی اشیا حالت مستطیل در نظر گرفته شده که میتوانید آنها را بازنویسی (override) کنید).
- Move: این متد به عنوان پارامتر یک نقطه را گرفته و شی مورد نظر را به آن نقطه منتقل میکند در واقع نقطه شروع و پایان ترسیم شی را تغییر میدهد.
- Move: این متد نیز برای جابجایی شی به کار میرود، این متد دارای پارامترهای جابجابی در راستای محور Xها , جابجایی در راستای محور Yها؛ و شی مورد نظر را به آن نقطه منتقل میکند در واقع نقطه شروع و پایان ترسیم شی را با توجه به پارامترهای ورودی تغییر میدهد.
- Resize: این متد نیز برای تغییر اندازه شی به کار میرود، این متد دارای پارامترهای تغییر اندازه در راستای محور Xها , تغییر اندازه در راستای محور Yها میباشد و نقطه پایان شی مورد نظر را تغییر میدهد اما نقطه شروع تغییری نمیکند.
- Resize: این متد نیز برای تغییر اندازه شی به کار میرود، در زمان تغییر اندازه شی با ماوس ابتدا یک نقطه شروع وجود دارد که ماوس در آن نقطه کلیک شده و شروع به درگ کردن شی جهت تغییر اندازه میکند (پارامتر اول این متد نقطه شروع درگ کردن جهت تغییر اندازه را مشخص میکند startPoint)، سپس در یک نقطه ای درگ کردن تمام میشود در این نقطه باید شی تغییر اندازه پیدا کرده و ترسیم شود ( پارامتر دوم این متد نقطه مذکور میباشد currentLocation). سپس با توجه با این دو نقطه بررسی میشود که تغییر اندازه در کدام جهت صورت گرفته است و اعداد جهت تغییرات نقاط شروع و پایان شی مورد نظر محاسبه میشوند. (مثلا تغییر اندازه در مستطیل از ضلع بالا به طرفین، یا از ضلع سمت راست به طرفین و ....). البته برای مربع و دایره باید کاری کنیم که طول و عرض تغییر اندازه یکسان باشد.
- CalulateLocationAndSize: این متد که در سازنده کلاس استفاده شده در واقع دو نقطه شروع و پایان را گرفته و با توجه به تصویر ابتدای پست حالتهای 1 و 2 و3 و 4 را به حالت 1 تبدیل کرده و StartPoint و EndPoint را اصلاح میکند.
- SetBackgroundBrushAsHatch: این متد یک الگوی Brush گرفته و با توجه به رنگ پس زمینه شی خصوصیت BackgroundBrush را مقداردهی میکند.
- SetBackgroundBrushAsLinearGradient: این متد با توجه به خصوصیت ForeColor و BackgroundColor یک Gradiant Brush ساخته و آن را به خصوصیت
BackgroundBrush نسبت میکند. - SetBackgroundBrushAsSolid: یک الگوی پر کردن توپر برای شی مورد نظر با توجه به خصوصیت BackgroundColor شی ایجاد کرده و آن را به خصوصیت BackgroundBrush شی نسبت میدهد.
تذکر: متدهای Move، Resize و HasPointInShape به صورت virtual تعریف شده تا کلاسهای مشتق شده در صورت نیاز خود کد رفتار مورد نظر خود را override کرده یا از همین رفتار استفاده نمایند.
خوشحال میشم در صورتی که در Refactoring کد نوشته شده با من همکاری کنید.
در پستهای آینده به بررسی و پیاده سازی دیگر کلاسها خواهیم پرداخت.
JQuery 1.3 ارائه شد
روز قبل، نگارش رسمی و نهایی jQuery 1.3 ارائه شد و بلافاصله هم فایل مخصوص آن جهت بکارگیری intellisense ویژوال استودیو، توسط علاقمندان تهیه و عرضه گردید.
تازههای این نگارش:
- Sizzle : یک موتور CSS selector قدرتمند (400 درصد بهبود سرعت نسبت به عملکرد کتابخانه قبلی در این مورد)
- بازنگری در نحوه مدیریت رخدادها
- موتور تزریق HTML بسیار سریع (تا 15 برابر سریعتر نسبت به کتابخانه قبلی)
- موتور بسیار سریع موقعیت یابی
- تشخیص نوع مرورگر در آن متوقف شده و بجای آن از تشخیص ویژگیها برای ماندگاری بیشتر این کتابخانه در سالهای آتی استفاده گردیده است. (بجای بررسی userAgent ، از باگهای شناخته شده یا ویژگیهای خاص، جهت تشخیص مرورگر استفاده میشود تا بازه وسیعی از محصولات مشابه را بتوانند پوشش دهند)
- جهت مشاهده و آزمایش بهبود کارآیی این کتابخانه لطفا به این صفحه مراجعه کنید. (لیست کامل این آزمایشات در انتهای صفحه لیست تغییرات، قابل دسترسی است)
مانند همیشه این کتابخانه در دو نگارش فشرده شده (جهت استفاده در سایتها) و نگارش عادی و حجیمتر (جهت برنامه نویسی و دیباگ کردن کدها) ارائه شده است. چون کدها در Google code هاست شده احتمالا دسترسی به آن مشکل خواهد بود. این مجموعه را از این آدرس میتوانید دریافت کنید.
توصیه میشود از سرورهای سریع گوگل بعنوان هاست کتابخانه جیکوئری سایتهای خود استفاده کنید. برای این منظور از آدرس زیر میتوان استفاده کرد:
اگر قصد اجرای برخی کارها به صورت زمانبندی شده و در فواصل زمانی مشخص را دارید، این مقاله به شما کمک خواهد کرد تا به بهترین شکل ممکن آن را انجام دهید. کارهایی مانند ارسال خبرنامه، فرستادن SMS تبریک تولد یا هماهنگ سازی دادهها بین دو منبع داده از جمله اَعمالی هستند که باید به صورت زمانبندی شده انجام شوند.
کتابخانهی Quartz.NET، از کتابخانه ای با نام Quartz و از زبان Java به NET. منتقل شده است. Quartz.NET، رایگان و باز متن است و از طریق آدرس http://quartznet.sourceforge.net در دسترس است. از طریق NuGet نیز میتوانید با تایپ عبارت quartz در فرم مربوطه، این کتابخانه را نصب کنید. این کتابخانه را در برنامههای Desktop و Web (حتی یک Shared Server) تست کردم و به خوبی انجام وظیفه میکند.
شروع کار با Quartz.NET
ضمن در اختیار قرار دادن امکانات فوق العاده و انعطاف پذیری بسیار، کار با این کتابخانه آسان و از فرایندی منطقی تبعیت میکند. فرایند اجرای یک روال زمانبندی شده از طریق Quartz.NET، از چهار مرحلهی اصلی تشکیل شده است.
1) پیاده سازی اینترفیس IJob
2) مشخص کردن جزئیات روال با اینترفیس IJobDetail
3) مشخص کردن تنظیمات زمان با استفاده از اینترفیس ITrigger
4) مدیریت اجرا با استفاده از اینترفیس IScheduler
مثالی را بررسی میکنیم. در این مثال قصد داریم تا عبارتی را همراه با تاریخ و زمان جاری در یک فایل ذخیره کنیم. این پیغام باید 3 بار و در فواصل زمانی 10 ثانیه به فایل اضافه شود. در پایان، فایلی خواهیم داشت که در سه خط، یک عبارت، همراه با تاریخ و زمانهای مختلف را که 10 ثانیه با یکدیگر اختلاف دارند در خود ذخیره کرده است. ابتدا کار زمانبندی شده را با ارائهی پیاده سازی برای متد Execute اینترفیس IJob این کتابخانه ایجاد میکنیم. وارد کردن فضای نام Quartz را فراموش نکنید.
namespace SchedulerDemo.Jobs { using System; using System.IO; using Quartz; public class HelloJob : IJob { public void Execute(IJobExecutionContext context) { // for web apps // string path = System.Web.Hosting.HostingEnvironment.MapPath("~/Data/Log.txt"); // for desktop apps string path = @"C:\Log.txt"; using (StreamWriter sw = new StreamWriter(path, true)) { sw.WriteLine("Message from HelloJob " + DateTime.Now.ToString()); } } } }
حال، زمان انجام تنظیمات مختلف برای اجرای روال مربوطه است. بهتر است تا interfaceیی ایجاد و متدی با نام Run در آن داشته باشیم.
namespace SchedulerDemo.Interfaces { public interface ISchedule { void Run(); } }
حال، پیاده سازی خود را برای این interface ارائه میدهیم.
namespace SchedulerDemo.Jobs { using System; using Quartz; using Quartz.Impl; using SchedulerDemo.Interfaces; using SchedulerDemo.Jobs; public class HelloSchedule : ISchedule { public void Run() { //DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 2); DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second); IJobDetail job = JobBuilder.Create<HelloJob>() .WithIdentity("job1") .Build(); ITrigger trigger = TriggerBuilder.Create() .WithIdentity("trigger1") .StartAt(startTime) .WithSimpleSchedule(x => x.WithIntervalInSeconds(10).WithRepeatCount(2)) .Build(); ISchedulerFactory sf = new StdSchedulerFactory(); IScheduler sc = sf.GetScheduler(); sc.ScheduleJob(job, trigger); sc.Start(); } } }
معرفی فضاهای نام Quartz و Quartz.Impl را فراموش نکنید.
از حالا، به روالی که قرار است به صورت زمانبندی شده اجرا شود، "وظیفه" میگوییم.
ابتدا باید مشخص کنیم که وظیفه در چه زمانی پس از اجرای برنامه شروع به اجرا کند. از آنجا که پایه و اساس زمانبندی، بر تاریخ و ساعت استوار است، کتابخانهی Quartz.NET، روشها و امکانات بسیاری را برای تعیین زمان در اختیار قرار میدهد. با بررسی تمامی آنها، سادهترین و منعطفترین را به شما معرفی میکنم. کلاس DateBuilder که همراه با Quartz.NET وجود دارد، امکان تعیین زمان را به اَشکال مختلف میدهد. در خط 14، از متد FutureDate این کلاس استفاده شده است که خوانایی بهتری نسبت به بقیهی متدها دارد. پارامتر اول این متد، عدد، و پارامتر دوم، واحد زمانی را میپذیرد.
DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second);
در اینجا، زمان آغاز وظیفه را 2 ثانیه پس از آغاز برنامه تعریف کرده ایم. واحدهای زمانی دیگر شامل میلی ثانیه، دقیقه، ساعت، روز، ماه، هفته و سال هستند. کلاس DateBuilder، متدهای مختلفی برای تعیین زمان را در اختیار قرار میدهد. تعیین زمان آغاز به روش دیگر را به صورت کامنت شده در خط 13 مشاهده میکنید.
وظیفهی ایجاد شده در خط 16 تا 18 معرفی شده است.
IJobDetail job = JobBuilder.Create<HelloJob>() .WithIdentity("job1") .Build();
پشتیبانی Quartz.NET از سینتکس fluent، کدنویسی را ساده و لذت بخش میکند. با استفاده از متد Create کلاس JobBuilder، وظیفه را معرفی میکنیم. متد Create، یک متد Generic است که نام کلاسی که اینترفیس IJob را پیاده سازی کرده است میپذیرد. یک نام را با استفاده از متد WithIdentity به وظیفه نسبت میدهیم (البته این کار، اختیاری است) و در انتها، متد Build را فراخوانی میکنیم. خروجی متد Build، از نوع IJobDetail است.
و حالا نوبت به تنظیمات زمان رسیده است. در Quartz.NET، این مرحله، "ایجاد trigger" نام دارد. خطوط 20 تا 24 به این کار اختصاص دارند.
ITrigger trigger = TriggerBuilder.Create() .WithIdentity("trigger1") .StartAt(startTime) .WithSimpleSchedule(x => x.WithIntervalInSeconds(10).WithRepeatCount(2)) .Build();
ابتدا متد Create کلاس TriggerBuilder را فراخوانی میکنیم، سپس با استفاده از متد WithIdentity، یک نام به trigger اختصاص میدهیم (البته این کار، اختیاری است). با متد StartAt، زمان شروع وظیفه را که در ابتدا با استفاده از کلاس DateBuilder ایجاد کردیم تعیین میکنیم. مهمترین قسمت، تعیین دفعات و فواصل زمانی اجرای وظیفه است. همان طور که احتمالاً حدس زده اید، Quartz.NET مجموعه ای غنی از روشهای مختلف برای تعیین بازهی زمانی اجرا را در اختیار قرار میدهد. آسانترین راه، استفاده از متد WithSimpleSchedule است. با استفاده از یک عبارت Lambda که ورودی آن از نوع کلاس SimpleScheduleBuilder است، دفعات و فواصل زمانی اجرا را تعیین میکنیم. متد WithIntervalInSeconds، برای تعیین فواصل زمانی در بازهی ثانیه استفاده میشود. متد WithRepeatCount نیز برای تعیین دفعات اجرا است. وظیفهی ما، 3 مرتبه و در فواصل زمانی 10 ثانیه اجرا میشود. مطمئن باشید اشتباه نکردم! بله، سه مرتبه. تعداد دفعات اجرا برابر است با عددی که برای متد WithRepeatCount تعیین میکنید، به علاوهی یک. منطقی است، چون مرتبهی اول اجرا زمانی است که با استفاده از متد StartAt تعیین کرده اید. در پایان، متد Build را فراخوانی میکنیم. خروجی متد Build، از نوع ITrigger است.
آخرین کار (خطوط 26 تا 30)، ایجاد شی از اینترفیس IScheduler، فراخوانی متد ScheduleJob آن، و پاس دادن اشیای job و trigger که در قسمت قبل ایجاد شده اند به این متد است. در انتها، متد ()Start را برای آغاز وظیفه فراخوانی میکنیم.
ISchedulerFactory sf = new StdSchedulerFactory(); IScheduler sc = sf.GetScheduler(); sc.ScheduleJob(job, trigger); sc.Start();
حال شما یک وظیفه تعریف کرده اید که در هر جای برنامه به صورت زیر، قابل فراخوانی است.
ISchedule myTask = new HelloSchedule(); myTask.Run();
کتابخانه ای که با آن سر و کار داریم بسیار غنی است و امکانات بسیاری دارد. در قسمت بعد، با برخی امکانات دیگر این کتابخانه آشنا میشوید.
DSL یا Domain Specific languages به معنی زبانهایی با دامنه محدود است که برای اهداف خاصی نوشته میشوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبانها به شما اجازه نمیدهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه میدهند به هدفی که برای آن نوشته شدهاند، برسید. یکی از این زبانها همان css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المانهای وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدودهای مشخص برای خود دارد. به این نوع از زبانهای DSL، نوع اکسترنال هم میگویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interfaceها که به ما شیوه آسانی از دسترسی به جنبههای یک شیء را میدهد. برای آشنایی هر چه بیشتر با این زبانها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه میشود.
Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.
از دیرباز سیستمهای Ant و Maven وجود داشتند و کار آنها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آنها و افزودن ویژگیهای قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگیهای گریدل میتوان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستمها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگینها داشت و گوگل با استفاده از این قابلیت، پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگینها اجرا میشوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و میتوان آن را در پروژههای جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی میکنند.
گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG استفاده میکند و طبق آن ترتیب وظایف یا taskها را دانسته و آنها را اجرا میکند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام میدهد.
- در مرحله آماده سازی ما به گریدل میگوییم چه پروژه یا پروژههایی نیاز به بیلد شدن دارند. در اندروید استادیو، این مرحله در فایل settings.gradle انجام میشود؛ شما در این فایل مشخص میکنید چه پروژههای نیاز به بیلد شدن توسط گریدل دارند. ساختار این فایل به این شکل است:
include ':ActiveAndroid-master', ':app', ':dbutilities'
-
در اولین مرحله انتظار دارد که فایل settings در دایرکتوری جاری باشد و اگر آن را پیدا کرد آن را مورد استفاده قرار میدهد؛ در غیر اینصورت مرحله بعدی را آغاز میکند.
- در مرحله دوم، در این دایرکتوری به دنبال دایرکتوری به نام master میگردد و اگر در آن هم یافت نکرد مرحله سوم را آغاز میکند.
- در مرحله سوم، جست و جو در دایرکتوری والد انجام میشود
- چنانچه این فایل را در هیچ یک از احتمالات بالا نیابد، همین پروژه جاری را تشخیص خواهد داد.
include ':ActiveAndroid-master', ':app', ':dbutilities' project('dbutilities').projectDir=new File(settingsDir,'../dir1/dir2');
-
در مرحله پیکربندی، وظایف یا taskها را معرفی میکنیم. این عمل پیکربندی توسط فایل build.gradle که برای پروژه اصلی و هر زیر پروژهای که مشخص شدهاند، صورت میگیرد. در این فایل شما میتوانید خواص و متدهایی را تعریف و و ظایفی را مشخص کنید.
در پروژه اصلی، فایل BuildGradle شامل خطوط زیر است:
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' } } allprojects { repositories { jcenter() } }
در مرحله اجرا هم این وظایف را اجرا میکنیم. تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper میباشد انجام میشود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، میتوانید در ادامه دستور پیامهای مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
task mytask <<{ println ".net tips task in config phase" }
gradlew mytask
gradlew --info mytask
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، میتوانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیامها در کدام بخش چاپ میشوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ میشوند.
task mytask { println ".net tips task in config phase" doLast{ println ".net tips task in exe phase" } }
یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1، تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسکها یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگیها تنها به تسکهای داخل فایل گریدل انجام میشود و نه تسکهای پلاگینها یا وابستگی هایی که تعریف میکنیم.
task mytask1 << { println ".net tips is the best" } task mytask2() { dependsOn mytask1 doLast{ Date time=Calendar.getInstance().getTime(); SimpleDateFormat formatter=new SimpleDateFormat("HH:mm:ss , YYYY/MM/dd"); println "mytask1 is done at " + formatter.format(time); } }
gradlew --info mytask2
Executing task ':app:mytask1' (up-to-date check took 0.003 secs) due to: Task has not declared any outputs. خروجی تسک شماره یک .net tips is the best :app:mytask1 (Thread[main,5,main]) completed. Took 0.046 secs. :app:mytask2 (Thread[main,5,main]) started. :app:mytask2 Executing task ':app:mytask2' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs. خروجی تسک شماره دو mytask1 is done at 04:03:09 , 2016/07/07 :app:mytask2 (Thread[main,5,main]) completed. Took 0.075 secs. BUILD SUCCESSFUL
در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسکهای آماده ای نیز دارد که میتوانید در مستندات آن بیابید. به عنوان مثال یکی از تسکهای مفید و کاربردی آن تسک کپی کردن هست که از طریق آن میتوانید فایلی یا فایلهایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسکهای خود را به شکل زیر به شیوه اکشن بنویسید:
task mytask(type:Copy) { dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
برای نمایش تسکهای موجود میتوانید از گریدل درخواست کنید که لیست تمامی تسکهای موجود را به شما نشان دهد. برای اینکار میتوانید دستور زیر را صدا کنید:
gradlew --info tasks
Other tasks ----------- clean jarDebugClasses jarReleaseClasses mytask mytask2 transformResourcesWithMergeJavaResForDebugUnitTest transformResourcesWithMergeJavaResForReleaseUnitTest
task mytask(type:Copy) { description "copy apk files to root directory" dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
یکی دیگر از نکات جالب در مورد گریدل این است که میتواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کردهایم که به ما این اجازه را میدهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه میشود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ میکند.
tasks.whenTaskAdded{ task-> println "task is added $task.name" }
گریدل امکانات دیگری چون بررسی استثناءها و ایجاد استثناءها را هم پوشش میدهد که میتوانید در این صفحه آن را پیگیری کنید.
Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژهای که روی سیستم شما قرار بگیرد از نسخهای متفاوتی از گریدل استفاده کند، باعث میشود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخهای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا میزنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده میشود و wrapper به خوبی میداند که به چه نسخهای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم میکند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید میتوانید این دو فایل را ببینید.
از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژهای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
gradle wrapper --gradle-version 2.4
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
\gradle\wrapper\gradle-wrapper.properties
اینها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشنتر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.
تزریق وابستگیها
الان در قسمتی هستید که صرفا تئوری کار در حال بررسی است. کمی تامل کنید، در قسمتهای بعد یک نمونه IoC Container خانگی توسعه داده میشود و بعد یک نمونه مورد استفاده در صنعت، کامل بررسی خواهد شد.
شما با ابزار شروع کردید. اینجا با تئوری و مفاهیم شروع شده، بعد به استفاده از ابزارها رسیده. ضمنا این مباحث برای خواندن حداقل 2 هفته است و نه یک روز. باید مطالعه کنید. تمرین کنید تا جا بیفتد.