اگه از نوشتن کد زیاد html برای ایجاد دیالوگ خسته شدید یا قصد دارید با عملیات کدنویسی آزادتری روی دبالوگها داشته باشید از این پلاگین استفاده کنید.
چند وقت پیش لینکی را معرفی کردم که در آن به طراحی پنجرههای بوت استرپ 3 با استفاده از جی کوئری پرداخته بود و از آنجا که من دوست دارم انعطاف بیشتری در استفاده از این مدل کتابخانهها داشته باشم و مستندات آن را حفظ نکنم، آنها را به HtmlHelper تبدیل میکنم.
ابتدا از این آدرس فایلهای مورد نظر را دریافت کنید. دو عدد از آنها فایل استایل و دیگری فایل جی کوئری آن است که به ترتیب زیر صدا بزنید:
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> <link href="~/content/css/bootstrap-dialog.min.css" rel=stylesheet"></link> <script src="~/Scripts/bootstrap-dialog.min.js"></script>
پروژه اصلی شامل دو فایل اصلی است؛ یکی که درفضای نام models جهت قرار دادن مدلها قرار گرفته و دیگری در فضای نام Controls جهت ایجاد متدهای Helper یا اجرایی قرار گرفته است.
ابتدا نیاز است که یک کلاس از نوع BootstrapDialog ایجاد کنید تا خصوصیات پنجره مشخص گردند. این خصوصیات به شرح زیر هستند:
var dialog=new BootstrapDialog(); dialog.Title="عنوان دیالوگ"; dialog.Message="متن پنجره"; //فعال سازی این خصوصیت باعث میشود یک دکمه بستن به //پنجره اضافه شده و همچنین توسط کلیک کاربر در خارج از صفحه //باعث بسته شدن پنجره شود یا استفاده از کلید //ESC dialog.Closable=false; //تغییر اندازه دیالوگ Dialog.Size=BootstrapDialogSize.SizeNormal; //رنگ بندی دیالوگ را تغییر میدهد.مقدار زیر باعث میشود //دیالوگ با رنگبندی قرمز نمایش داده شود تا برای نمایش خطاها مناسب باشد Dialog.Type=BootstrapDialogType.Danger; //برای اعمال کردن یک کلاس استابل دلخواه Dialog.CssClass=""; //آیکن برای دیالوگ-استفاده از نام کلاس آیکنهای بوت استراپ Dialog.SpinIcon=""; //یک توصیف است که فقط در کد صفحه نمایش داده میشود //استفاده خاصی ندارد Dialog.Description=""; //بعد از بستن دیالوگ ، کدهای آن در صفحه حذف خواهند شد //اگر میخواهید کد را بارها و بارها نمایش دهید //آن را با مقدار ناصحیح مقدار دهی کنید dialog.AutoDestory=false; //========== رویدادها ============= //این رویدا قبل از نمایش دیالوگ نمایش داده میشود dialog.OnShow="function(){alert('before Dialog');}"; //این رویداد بعد از نمایش دیالوگ اجرا میشود dialog.OnShown="function(){alert('after Dialog shown');}"; //موقع درخواست بستن دیالوگ قبل از بسته شدن اجرا میگردد dialog.OnHide="function(){alert('before Dialog close');}"; //بعد از بسته شدن دیالوگ اجرا میشود dialog.OnHidden="function(){alert('after Dialog close');}";
@{ var dialog=new BootstrapDialog(); dialog.... // ........ } @HTML.BootstrapDialog("example1",dialog)
در کد، اولین پارامتر نام پنجره است: از این اسم بعدا میتوانید جهت اجرای متدها، چه دستی توسط خود شما یا ایجاد متدهای ساده توسط خود کلاس استفاده کنید. دومین پارامتر هم دریافت خصوصیات پنجره است که در بالا توضیح دادیم.
دکمه ها
در صورتیکه قصد دارید دکمهای را روی پنجره ایجاد نمایید، با شیوه زیر اینکار صورت میگیرد:
var dialog=new BootstrapDialog(); var cancelButton=new BootstrapDialogButton("cancelButton"); //cancelButton.id="cancelButton"; cancelButton.label="Cancel"; cancelButton.Key=65; cancelButton.Action="function(){alert('You Clicked!');}"; dialog.AddButton(cancelButton);
dialog.RemoveButton("cancelButton");
داده ها
در صورتیکه قصد دارید دادههایی را به این پنجره نسبت دهید تا بعدا در کدهای سمت کلاینت از آن استفاده کنید میتوانید از کد زیر استفاده کنید:
dialog.AddData("key","value");
dialog.RemoveData("key");
متدها
متدها را به دو صورت میتوانید اعمال کنید:
- دستی: که میتوانید اطلاعات متدها را در همان صفحه مثال و مستندات ببینید و از نامی که به دکمهها و پنجرهها میدهید آنها را اعمال کنید.
- با استفاده از کلاس: کلاس ما شامل دو متد دیگر برای کنترل متدها میباشد. حدود 13 متد در آن پشتیبانی میشود که باعث میشود در بسیاری از اوقات دیگری نیازی به دانستن نام متدها نداشته باشید. یکی از متدها برای استفاده در Helper طراحی شده است که خروجی آن از نوع MvcHtmlString است و متد دیگر خروجی string دارند که میتوانید در صورتیکه خواستید، در رویدادها و خارج از Html Helper از آن استفاده کنید.
نحوهی استفاده از helper به شکل زیر است؛ فرض شده است که پنجره را تشکیل دادهاید و الان قصد دارید با کلیک بر روی یک دکمه آن را نمایش دهید:
$( "#btnshowpopup" ).click(function() { @HTML.RunBootstrapDialogMethod("example1",BootstrapDialogMethods.Open) });
$( "#btnshowpopup" ).click(function() { @HTML.RunBootstrapDialogMethod("example1",BootstrapDialogMethods.SetData,new{"key","value"}) });
cancelButton.Action="function(){{{0}}}"; cancelButton.Action=string.format(cancelButton.Action,RunBootstrapDialogMethod("example1",BootstrapDialogMethods.Close));
به عنوان یک مثال نهایی کد زیر را نوشته که نتیجه آن را در تصویر زیر میبینم:
@{ const string dialogName = "errorDialog"; var cancelButton = new BootstrapDialogButton(); cancelButton.Id = "btncancel"; cancelButton.Label = "بستن"; cancelButton.Action = "function(){{{0}}}"; cancelButton.Action = String.Format(cancelButton.Action, Dialogs.RunBootstrapDialogMethod(dialogName, BootstrapDialogMethods.Close)); var dialog = new BootstrapDialog(); dialog.AddButton(cancelButton); dialog.Title = "عنوان"; dialog.Message = "پیام هشدار"; dialog.DialogType=BootstrapDialogType.Warning; dialog.DialogSize=BootstrapDialogSize.SizeNormal; dialog.Closable = false; dialog.AddData("data1","5"); } @Html.BootstrapDialog(dialogName, dialog) @Html.RunBootstrapDialogMethod(dialogName,BootstrapDialogMethods.Open);
نکته مهم: برای ایجاد پنجره از طریق توابع عمل کنید و خط تعریف پنجره را داخل یک تابع قرار داده و از همانجا آن را باز کنید. در حال حاضر به نظر میرسد در صورتی که تعریف پنجره به طور عمومی باشد، این کتابخانه برای بار دوم به بعد مشکلاتی دارد که مشکل آن بسته نشدن پنجره است. در حال حاضر در گیت هاب این مسئله را عنوان کردیم، در صورتی که پاسخی ارائه شود همینجا به اطلاع شما میرسانم.
اگر قصد طراحی رابط کاربری با بوت استراپ دارید و میخواهید یک پیش نمایش از طرح را با فتوشاپ ایجاد کنید، این لینک به شما قالبی از اشیایی با استایل بوت استراپ را می دهد، نسخه کامل آن شامل هزینه میشود.
اگر جدیدا قصد برنامه نویسی اندروید را کردهاید، یا هنوز روشهای متدوالی را برای
کار با این زبان انتخاب نکردهاید؛ به نظرم این مقاله میتواند کمک خوبی
برای شما باشد. مسائلی که بیان میکنم در واقع از تجربیات شخصی و راه حل
هایی است که برای خودم تعیین کردهام و تعدادی از آنها را در طول مدتی که
در این زمینه فعالیت کردهام، از جاهای مختلف دیده و در یک جا گردآوری
کردهام. برای نامگذاری اشیاء و متغیرها و دیگر موارد، من از این قاعده پیروی میکنم که به نظرم بسیار ایده آل میباشد. الگوی معماری هم که جدیدا مورد استفاده قرار دادهام، الگوی MVP است که نمونهای از آن، در گیت هاب قرار گرفته است. البته این مثال ساده تر نیز وجود دارد. تشریح کامل این معماری را به همراه آزمون واحد آن، میتوانید در این مقاله سه قسمتی ببینید.
در اینجا، یک سری نکات را در طول برنامه نویسی، متذکر میشوم تا مدیریت کدهای شما را در اندروید راحتتر کند.
یک نکتهی دیگر را که باید متذکر شوم این است که همه اصطلاحاتی که در این مقاله استفاده میشوند بر اساس اندروید استادیو و مستندات رسمی گوگل است است؛ به عنوان نمونه عبارتهای ماژول و پروژه آن چیزی هستند که ما در اندروید استادیو به آنها اشاره میکنیم، نه آنچه که کاربران Eclipse به آن اشاره میکنند.
یک. برای هر تکه کد و یا متدی که مینویسید مستندات کافی قرار دهید و اگر این متد نیاز به مجوز خاصی دارد مانند نمونه زیر، آن را حتما ذکر کنید:
در اینجا، یک سری نکات را در طول برنامه نویسی، متذکر میشوم تا مدیریت کدهای شما را در اندروید راحتتر کند.
یک نکتهی دیگر را که باید متذکر شوم این است که همه اصطلاحاتی که در این مقاله استفاده میشوند بر اساس اندروید استادیو و مستندات رسمی گوگل است است؛ به عنوان نمونه عبارتهای ماژول و پروژه آن چیزی هستند که ما در اندروید استادیو به آنها اشاره میکنیم، نه آنچه که کاربران Eclipse به آن اشاره میکنند.
یک. برای هر تکه کد و یا متدی که مینویسید مستندات کافی قرار دهید و اگر این متد نیاز به مجوز خاصی دارد مانند نمونه زیر، آن را حتما ذکر کنید:
/** * * <p> * check network is available or not <br/> * internet connection is not matter,for check internet connection refer to IsInternetConnected() Method in this class * </p> * <p> * Required Permission : <b>android.permission.ACCESS_NETWORK_STATE</b> * </p> * @param context * @return returns true if a network is available */ public boolean isNetworkAvailable(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); }
همچنین اگر، مورد خاص دیگری مثل بالا بود، حتما آن را ذکر کنید. میتوانید از تگ گذاری در کامنت ها
نیز استفاده کنید. از ویژگیهای کامنت todo در اندروید استادیو این است
که میتوانید در حین کار با سیستم گیت نیز از آن بهره ببرید و قبل از کامیت
کردن کد، کدهای todo به شما یادآوری شوند و هر پیکربندی را که لازم دارید، روی آن انجام دهید.
دو.
از یک کلاس واحد جهت استفاده از اطلاعات عمومی و یا ثابتها استفاده
نمایید. این اطلاعات میتوانند شامل: مسیرها، آدرسهای وب سرویس، شماره
اختصاصی هر نوتیفیکیشن و .... باشند. برای اینکار میتوان هر کدام از اطلاعات را
داخل یک کلاس قرار داد و همه این کلاسها را به صورت استاتیک تعریف کنید تا
بدین شکل در دسترس قرار بگیرند (از الگوی singleton هم میتوان استفاده
کرد).
public class ProjectSettings { public static NotificationsId=new NotificationsId(); public static UrlAddresss=new UrlAddresss(); public static SdPath=new SdPath(); ...... }
ProjectSettings.NotificationsId.UpdateNotificationId
بدین شکل هم به طور ساده و مفهومی صدا زده میشود و هم اینکه در همه جای
برنامه این ثابتها و مقادیر قابل استفاده هستند. به عنوان مثال به شماره
هر نوتیفیکیشن از همه جا دسترسی دارید و هم اینکه شمارهای تکراری اشتباها
انتخاب نمیشود.
سه. حداکثر استفاده از اینترفیس را به خصوص برای UI انجام بدهید:
به عنوان نمونه، بسیاری نمایش یک toast را به شکل زیر انجام میدهند:
سه. حداکثر استفاده از اینترفیس را به خصوص برای UI انجام بدهید:
به عنوان نمونه، بسیاری نمایش یک toast را به شکل زیر انجام میدهند:
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
یا اینکه برای یک دیالوگ مستقیما و در جا همانجا به کدنویسی مشغول میشوند.
این روشها هیچ مشکلی ندارند ولی در آینده نگهداری کد را مشکل میکنند.
مثلا تصور کنید شما بسیاری از جاهای برنامه، Toast زدید و حالا قصد دارید در
نسخه بعدی برنامه، toastهای دلخواه و یا custom ایی را ایجاد کنید. در این صورت
مجبورید کل برنامه را رصد کرده و هر جا toast هست آن را تغییر دهید. در
اینجا هم اصول DRY را نادیده گرفتهاید و هم زحمت شما زیاد شدهاست و حتی
ممکن است یک یا چندتایی از قلم بیفتند. برای دیالوگها هم بدین صورت خواهد
بود و خیلی از مسائل دیگر. به همین جهت استفاده از اینترفیسها توصیه
میشود و فردا نیز اگر باز یک کلاس دیگر را نوشتید، خیلی راحت آن را با کلاس
فعلی تعویض میکنید.
public interface IMessageUI { void ShowToast(Context context,String message); } public class MessageUI impelement IMessageUI { public void ShowToast(Context context,string message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } }
چهار. اگر برای اولین بار است وارد اندروید میشوید، خوب چرخههای یک شیء، چون اکتیویتی یا فراگمنت را یاد بگیرید تا در آینده با مشکلات خاصی روبرو نشوید.
به عنوان مثال درست است که اولین رویداد فراخوانی در onCreate رخ میدهد ولی
همیشه محل مناسبی برای دریافت دیتاها در زمان اولیه نیست. به عنوان مثال
تصور کنید که لیستی در اکتیویتی A دارید و به اکتیویتی B میروید و یک آیتم
به اطلاعات اضافه میشود و موقعی که به اکتیویتی A بر میگردید، زیاد تعجب نکنید که لیست دقیقا به
همان شکل قبلی است و خبری از آیتم جدید نیست.
چون اکتیویتی در حالت stop بوده و بعد از آن به حالت Resume رفته و تا موقعی که این اکتیویتی از حافظه خارج نشود یا گوشی چرخش نداشته باشد، واکشی دیتاها صورت نخواهد گرفت. پس بهترین مکان در این حالت، رویداد OnStart است که در هر دو وضعیت صدا زده میشود؛ یا اینکه در OnRestatr روی آداپتور تغییرات جدید را اعمال کنید تا نیازی به واکشی مجدد دادهها نباشد.
چون اکتیویتی در حالت stop بوده و بعد از آن به حالت Resume رفته و تا موقعی که این اکتیویتی از حافظه خارج نشود یا گوشی چرخش نداشته باشد، واکشی دیتاها صورت نخواهد گرفت. پس بهترین مکان در این حالت، رویداد OnStart است که در هر دو وضعیت صدا زده میشود؛ یا اینکه در OnRestatr روی آداپتور تغییرات جدید را اعمال کنید تا نیازی به واکشی مجدد دادهها نباشد.
به طور خلاصه نحوه اجرای رویدادها بدین شکل است که ابتدای رویداد OnCreate اجرا میشود که هنوز هیچ UI ئی در آن پیاده سازی نشدهاست و شما در اینجا موظفید Layout خود را معرفی کنید. رویداد OnStart بعد از آن موقعی که UI آماده شده است، اجرا میگردد. سپس رویداد OnResume اجرا میشود.
تا بدینجا اکتیویتی مشکلی ندارد و میتواند به عملیات پاسخ دهد ولی اگر قسمتی از اکتیویتی در زیر لایهای از UI پنهان شود، به عنوان مثال دیالوگی باز شود که قسمتی از اکتیویتی را بپوشاند و یا منویی همانند تلگرام قسمتی از صفحه را بپوشاند، اکتیویتی اصطلاحا در حالت Pause قرار گرفته و بدین ترتیب رویداد OnPause اجرا میگردد. اگر همین دیالوگ بسته شود و مجددا اکتیویتی به طور کامل نمایان گردد مجددا رویداد OnResume اجرا میگردد.
از رویداد Onresume میتوانید برای کارهایی که بین زمان آغاز اکتیویتی و برگشت اکتیویتی مشترکند استفاده کرد. اگر به هر نحوی اکتیویتی به طور کامل پنهان شود٬، به این معناست که شما به اکتیویتی دیگری رفتهاید رویداد OnStop اجرا شدهاست و در صورت بازگشت، رویداد OnRestart اجرا خواهد شد. ولی اگر مدت طولانی از رویداد OnStop بگذرد احتمال اینکه سیستم مدیریت منابع اندروید، اکتیویتی شما را از حافظه خارج کند زیاد است و رویداد OnDestroy صورت خواهد گرفت. در این حالت دفعه بعد، مجددا همه عملیات از ابتدا آغاز میگردند.
تا بدینجا اکتیویتی مشکلی ندارد و میتواند به عملیات پاسخ دهد ولی اگر قسمتی از اکتیویتی در زیر لایهای از UI پنهان شود، به عنوان مثال دیالوگی باز شود که قسمتی از اکتیویتی را بپوشاند و یا منویی همانند تلگرام قسمتی از صفحه را بپوشاند، اکتیویتی اصطلاحا در حالت Pause قرار گرفته و بدین ترتیب رویداد OnPause اجرا میگردد. اگر همین دیالوگ بسته شود و مجددا اکتیویتی به طور کامل نمایان گردد مجددا رویداد OnResume اجرا میگردد.
از رویداد Onresume میتوانید برای کارهایی که بین زمان آغاز اکتیویتی و برگشت اکتیویتی مشترکند استفاده کرد. اگر به هر نحوی اکتیویتی به طور کامل پنهان شود٬، به این معناست که شما به اکتیویتی دیگری رفتهاید رویداد OnStop اجرا شدهاست و در صورت بازگشت، رویداد OnRestart اجرا خواهد شد. ولی اگر مدت طولانی از رویداد OnStop بگذرد احتمال اینکه سیستم مدیریت منابع اندروید، اکتیویتی شما را از حافظه خارج کند زیاد است و رویداد OnDestroy صورت خواهد گرفت. در این حالت دفعه بعد، مجددا همه عملیات از ابتدا آغاز میگردند.
پنج.
سرویس را با تردهای UI ترکیب نکنید. بعضا دیده میشود که کاربران
AsyncTask را داخل سرویس استفاده میکنند ولی این را بدانید که سرویس یک
ترد پردازشی جداگانه است و تضمینی برای ارتباط با UI به شما نمیدهند. هر
چند گوگل جدیدا تمهیداتی را برای آن اندیشیده است که به شما اجازه اینکار را
نمیدهد. ولی اگر باز هم اندروید استادیو به شما خوردهای نگرفت، خودتان این
قانون را اجرا کنید. قرار نیست یک AsyncTask با سرویس ترکیب شود.
شش. اگر برنامه شما قرار است در چندین حالت مختلفی که اتفاق میافتد، یک کار خاصی را انجام دهد، برای برنامهتان یک Receiver بنویسید و در آن کدهای تکراری را نوشته و در محلهای مختلف وقوع آن رویدادها، رسیور را صدا بزنید. برای نمونه برنامه تلگرام یک سرویس پیام رسان پشت صحنه دارد که در دو رویداد قرار است اجرا شوند. یکی موقعی که گوشی بوت خود را تکمیل کرده است و در حال آغاز فرایندهای سیستم عامل است و دیگر زمانی است که برنامه اجرا میشود. در اینجا تلگرام از یک رسیور سیستمی برای آگاهی از بوت شدن و یک رسیور داخل برنامه جهت آگاهی از اجرای برنامه استفاده میکند و هر دو به یک کلاس از جنس BroadcastReceiver متصلند:
<receiver android:name=".AppStartReceiver" android:enabled="true"> <intent-filter> <action android:name="org.telegram.start" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>
public class AppStartReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { ApplicationLoader.startPushService(); } }); } }
برنامه تلگرام حتی برای حالتهای پخش هم رسیورها استفاده کرده است که در همین رسیور وضعیت تغییر پلیر مشخص میشود:
<receiver android:name=".MusicPlayerReceiver" > <intent-filter> <action android:name="org.telegram.android.musicplayer.close" /> <action android:name="org.telegram.android.musicplayer.pause" /> <action android:name="org.telegram.android.musicplayer.next" /> <action android:name="org.telegram.android.musicplayer.play" /> <action android:name="org.telegram.android.musicplayer.previous" /> <action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.media.AUDIO_BECOMING_NOISY" /> </intent-filter> </receiver>
اینگونه تلگرام میتواند از همه جا سرویس را کنترل کند. مثلا موقعی که
دانلود یک موزیک تمام شده، سریعا پخش آن موزیک دانلود شده را آغاز کند.
هفت. اگر از یک ORM برای لایه دادهها استفاده میکنید (قبلا در سایت جاری در مورد ORMهای اندروید صحبت کردهایم و ORMهای خوش دستی که خودم از آنها استفاده میکنم ActiveAndroid و CPORM هستند که هم کار کردن با آنها راحت است و هم اینکه امکانات خوبی را عرضه میکنند) در این نوع ORMها شما نباید انتظار چیزی مانند EF را داشته باشید و در بعضی موارد باید کمی خودتان کمک کنید. به عنوان مثال در Active Android برای ایجاد یک inner join باید به شکل زیر بنویسید:
هفت. اگر از یک ORM برای لایه دادهها استفاده میکنید (قبلا در سایت جاری در مورد ORMهای اندروید صحبت کردهایم و ORMهای خوش دستی که خودم از آنها استفاده میکنم ActiveAndroid و CPORM هستند که هم کار کردن با آنها راحت است و هم اینکه امکانات خوبی را عرضه میکنند) در این نوع ORMها شما نباید انتظار چیزی مانند EF را داشته باشید و در بعضی موارد باید کمی خودتان کمک کنید. به عنوان مثال در Active Android برای ایجاد یک inner join باید به شکل زیر بنویسید:
From query= new Select() .from(Poem.class) .innerJoin(BankPoemsGroups.class) .on("poems.id=bank_poems_groups.poem") .where("BankGroup=?", String.valueOf(groupId)); return query.execute();
@Table(name="poems") public class Poem extends Model { public static String tableName="poems"; public static String codeColumn="code"; public static String titleColumn="title"; public static String bookColumn="book"; ...... @Column(name="code",index = true) public int Code; @Column(name="title") public String Title; @Column(name="book") public Book Book; .....}
From query= new Select() .from(Poem.class) .innerJoin(BankPoemsGroups.class) .on(Poem.TableName+"."+ Poem.IdColumn+"="+ BankPoemsGroups.TableName+"."+ BankPoemsGroups.PoemColumn) .where(Poem.BankGroupColumn+"=?", String.valueOf(groupId)); return query.execute();
حالا کمی بهتر شد. هم برای تغییر آینده بهتر شد و هم اینکه احتمال خطای
تایپی کاهش یافت. ولی باز هم ایجاد کوئری هنوز سخت است و نوشتن مرتب یک
رابطه جوین و شرطی و چسباندن مداوم رشتهها کار خسته کنندهای است و احتمال
خطای سهوی و انسانی هم در آن بالاست. برای رفع این مشکل بهتر است یک کلاس
جدید برای ساخت این کوئریها داشته باشیم که یک نمونه از آن را در این
پایین میبینید:
public class QueryConcater { public String GetInnerJoinQuery(String table1,String field1,String table2,String field2) { String query=table1 +"." +field1+"="+table2+"."+field2; return query; } ...... }
return new Select() .from(Color.class) .innerJoin(ProductItem.class) .on(queryConcater.GetInnerJoinQuery(ProductItem.TableName, ProductItem.ColorColumn, Color.TableName)) .where(queryConcater.WhereConditionQuery (ProductItem.TableName, ProductItem.ProductColumn), productId) .execute();
در دستورات بالا از این کلاس دو متد برای کوئری جوین و یکی هم برای ساخت
شرط ایجاد شده است و مقادیر به صورت پارامتر داده شدهاند. این الگو کمک
میکند که اگر هم این تکه کد اشتباه باشد، با تغییر یکجا بقیه کدها هم تغییر
میکنند و اگر در آینده هم ORM تغییر یافت، نحوه کوئری نویسیها در این کلاس
تغییر کنند، نه اینکه در طول لایه سرویس پراکنده باشند.
هشت. سعی کنید همیشه از یک سیستم گزارش خطا در اپلیکیشن خود استفاده کنید. در حال حاضر معروفترین سیستم گزارش خطا Acra است که میتوانید backend آن را هم از اینجا تهیه کنید و اگر هم نخواستید، سایت Tracepot امکانات خوبی را به رایگان برای شما فراهم میکند. از این پس با سیستم آکرا شما به یک سیستم گزارش خطا متصلید که خطاهای برنامه شما در گوشی کاربر به شما گزارش داده خواهد شد. این گزارشها شامل:
هشت. سعی کنید همیشه از یک سیستم گزارش خطا در اپلیکیشن خود استفاده کنید. در حال حاضر معروفترین سیستم گزارش خطا Acra است که میتوانید backend آن را هم از اینجا تهیه کنید و اگر هم نخواستید، سایت Tracepot امکانات خوبی را به رایگان برای شما فراهم میکند. از این پس با سیستم آکرا شما به یک سیستم گزارش خطا متصلید که خطاهای برنامه شما در گوشی کاربر به شما گزارش داده خواهد شد. این گزارشها شامل:
- وضعیت گوشی در حین باز شدن برنامه و در حین خطا چگونه بوده است.
- مشخصات گوشی
- این خطا به چه تعداد رخ داده است و برای چه تعداد کاربر
- گزارش گیری بر اساس اولین تاریخ رخداد خطا و آخرین تاریخ، نسخه سیستم عامل اندروید، ورژن برنامه شما و...
نه. آکرا همانند Elmah نمیتواند خطاهای catch شده را دریافت کند. برای حل این مشکل عبارت زیر را در catchها بنویسید:
ACRA.getErrorReporter().handleException(caughtException);
ده. بر خلاف سیستم دات نت که شما اجباری به استفاده از Try Catchها ندارید. در
جاوا اینگونه نیست و هر متدی که Throw روی آن انجام شده باشد مستلزم استفاده از catch است. به همین دلیل در شماره نه
گفتیم که چگونه باید این مشکل را حل کنیم. ولی در بسیاری از اوقات پیش
میآید که ما داریم از ماژولهای متفاوتی استفاده میکنیم که جدا از ماژول
اصلی برنامه هستند و این مورد باعث میشود که بعضی افراد یا Acra را در همه
ماژولها صدا بزنند یا اینکه بی خیال آن شوند. ولی کار راحتتر این است که
شما هم همانند برنامه نویسان جاوا متد خود را به Throw مزین کنید تا در
هنگام استفاده از آن در برنامه اصلی نیاز به catch شدن باشد. در واقع شما
نباید catchها را داخل یک کتابخانه جدا و مستقل قرار دهید و روش صحیح هم
همین است حالا چه استفاده از آکرا نیاز باشد و چه نباشد.
نمونه اشتباه:
public void CopyFile(String source,String destination,CopyFileListener copyFileListener) { try { InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination); long fileLength=new File(source).length(); // Transfer bytes from in to out byte[] buf = new byte[64*1024]; int len; long total=0; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); total+=len; copyFileListener.PublishProgress(fileLength,total); } in.close(); out.close(); } catch (IOException e) { e.printStackTrace(); } }
public void CopyFile(String source,String destination,CopyFileListener copyFileListener) throws IOException { InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination); long fileLength=new File(source).length(); // Transfer bytes from in to out byte[] buf = new byte[64*1024]; int len; long total=0; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); total+=len; copyFileListener.PublishProgress(fileLength,total); } in.close(); out.close(); }
مقدمه
موقعی که سینمای ناطق کار خود را آغاز
کرد، بسیاری از مردم از آن استقبال کردند و بسیاری از سینماگران که این
استقبال را دیدند، رفته رفته به سمت سینمای ناطق کشیده شدند. ولی در این بین
یک مشکلی ایجاد شده بود؛ اینکه ناشنوایان دیگر مانند قدیم یعنی دوران صامت
نمیتوانستند فیلمها را تماشا کنند، پس نیاز بود این مشکل به نحوی رفع شود. از اینجا بود که ایدهی زیرنویس شکل گرفت و این مشکل را رفع نمود. بعدها
فیلمها انتقال دهندهی فرهنگ و پیوند دهندهی مردم با فرهنگهای مختلف شدند
ولی تفاوت در زبان باعث میشد که این امر به خوبی صورت نگیرد. به همین علت
زیرنویس، وظیفهی دیگری را هم پیدا کرد و آن رساندن پیام فیلم با زبان خود
مخاطب بود. امروزه تهیهی زیرنویسها توسط بسیاری از افراد که با زبان انگلیسی
(آشنایی با یک زبان میانی برای ترجمه زیرنویس) آشنایی دارند رواج پیدا کرده
و روزانه نزدیک به صد زیرنویس یا گاها بیشتر با زبانهای مختلف بر روی
اینترنت قرار میگیرند. بزرگترین سایتی که در حال حاضر با شهرت جهانی در این
زمینه فعالیت دارد سایت subscene.com است.
آشنایی با انواع زیرنویسها
زیرنویسها فرمتهای مختلفی دارند مانند srt,sub idx,smi و ... ولی در حال حاضر معروفترین و معتبرترین فرمت در بین همهی فرمتها Subrip با پسوند SRT میباشد که قالب متنی به صورت زیر دارد:
203 00:16:38,731 --> 00:16:41,325 <i>Happy Christmas, your arse I pray God it's our last</i>
بررسی مشکل ما با زیرنویس در تلویزیونها
یکی از مشکلاتی
که ما در اجرای زیرنویسها بر روی تلویزیونها داریم این است که حروف
فارسی را به خوبی نمیشناسند و در هنگام نمایش با مشکل مواجه میشوند که
البته در اکثر مواقع با تبدیل زیرنویس از ANSI به Unicode یا UTF-8 مشکل حل
میشود. ولی در بعضی مواقع تلویزیون یا پلیرها از پشتیبانی زبان فارسی
سرباز میزنند و زیرنویس را به شکل زیر نمایش میدهند.
سلام = م ا ل س
به این جهت ما از یک برنامه به اسم
srttouni استفاده میکنیم که با استفاده یک روش جایگزینی و معکوس سازی، مشکل
ما را حل میکند. ولی باز هم این برنامه مشکلاتی دارد و از آنجا که برنامه
نویس این برنامه که واقعا کمال تشکر را از ایشان، دارم مشخص نیست، مجبور شدم
به جای گزارش، خودم این مشکلات را حل کنم.
مشکلات این برنامه :
- عدم حذف تگها ، گاها برنامه نویسها از تگ هایی چون Bold,italic,underline,color استفاده میکنند که معدود برنامههایی آن را پشتیبانی کرده و تلویزیون و پلیرها هم که اصلا پشتیبانی نمیکنند و باعث میشود که متن روی تلویزیون مثل کد html ظاهر شود
- بعضی جملات دوبار روی صفحه ظاهر میشوند.
- تنها یک فایل را در هر زمان تبدیل میکند. مثلا اگر یک سریال چند قسمته داشته باشید، برای هر قسمت باید زیرنویس را انتخاب کرده و تبدیل کنید، در صورتی که میتوان دستور داد تمام زیرنویسهای داخل دایرکتوری را تبدیل کرد یا چند زیرنویس را برای این منظور انتخاب کرد.
نحوهی خواندن زیرنویس با کدنویسی
با تشکر از دوست عزیز ما در این صفحه میتوان
گفت یک کد تقریبا خوب و جامعی را برای خواندن این قالب داریم. بار دیگر
نگاهی به قالب یک دیالوگ در زیرنویس میاندازیم و آن را بررسی میکنیم:
203 00:16:38,731 --> 00:16:41,325 <i>Happy Christmas, your arse I pray God it's our last</i>
کد زیر در کلاس SubRipServices وظیفهی خواندن محتوای فایل srt را بر اساس عبارتی که دادیم دارد:
در اولین خط ما یک Regular Expersion یا یک عبارت با قاعده تعریف کردیم که در اینجا میتوانید
با خصوصیات آن آشنا شوید. ما برای این کلاس یک الگو ایجاد کردیم و بر حسب
این الگو، متن یک زیرنویس را خواهد گشت و خطوطی را که با این تعریف جور در
میآیند و معتبر هستند، برای ما باز میگرداند.
private readonly static Regex regex_srt = new Regex(@"(?<sequence>\d+)\r\n(?<start>\d{2}\:\d{2}\:\d{2},\d{3}) --\> " + @"(?<end>\d{2}\:\d{2}\:\d{2},\d{3})\r\n(?<text>[\s\S]*?)\r\n\r\n", RegexOptions.Compiled); public string ToUnicode(string lines) { string subtitle= regex_srt.Replace(lines,delegate(Match m) { string text = m.Groups["text"].Value; //1.remove tags text = CleanScriptTags(text); //2.replace letters PersianReshape reshaper = new PersianReshape(); text = reshaper.reshape(text); string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); text = ""; foreach (string line in splitedlines) { //3.reverse tags text += ReverseText(reshaper.reshape(line))+Environment.NewLine ; } return string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value, m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ; } ); return subtitle; }
عبارتهایی که به صورت <name>? تعریف شدهاند در واقع یک
نامگذاری برای هر قسمت از الگوی ما هستند تا بعدا این امکان برای ما فراهم
شود که خطوط برگشتی را تجزیه کنیم که مثلا فقط قسمت متن را دریافت کنیم،
یا فقط قسمت زمان شروع یا پایان را دریافت کنیم و ...
متد tounicode یک آرگومان متنی دارد (lines) که شامل محتویات فایل
زیرنویس است. متد Replace در شی regex_srt با هر بار پیدا کردن یک متن بر
اساس الگو در رشته lines دلیگیتی را فرا میخواند که در اولین پارامتر آن
که از نوع matchEvaluator است، شامل اطلاعات متنی است که بر اساس الگو، یافت
شده است. خروجی آن از نوع string میباشد که با متن پیدا شده بر اساس الگو
جابجا خواهد کرد و در نهایت بعد از چندین بار اجرا شدن، کل متنهای تعویض
شده، به داخل متغیر subtitle ارسال خواهند شد.
کاری که ما در اینجا میکنیم این است که هر دیالوگ داخل زیرنویس را بر
اساس الگو، یافته و متن آن را تغییر داده و متن جدید را جایگزین متن قبلی
میکنیم. اگر زیرنویس ما 800 دیالوگ داشته باشد این دلیگیت 800 مرتبه اجرا
خواهد شد.
از آنجا که ما تنها میخواهیم متن زیرنویس را تغییر دهیم، در اولین
خط فرامین این دلیگیت تعریف شده، متن مورد نظر را بر اساس همان گروههایی
که تعریف کردهایم دریافت میکنیم و در متغیر text قرار میدهیم:
m.Groups["text"].Value
private static readonly Regex regex_tags = new Regex("<.*?>", RegexOptions.Compiled); private string CleanScriptTags(string html) { return regex_tags.Replace(html, string.Empty); }
PersianReshape reshaper = new PersianReshape(); text = reshaper.reshape(text); string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); text = ""; foreach (string line in splitedlines) { //3.reverse tags text += ReverseText(reshaper.reshape(line))+Environment.NewLine ; }
بلوک اول طبق گفتهی ویکی پدیا دستهی متنوعی از حروف مورد نیاز برای زبان فارسی، اردو، پاکستانی و تعدادی از زبانهای آسیای مرکزی است.
بلوک دوم شامل نمادها و نشانههای زبان
عربی است و در حال حاضر برای کد کردن استفاده نمیشوند و دلیل حضور آن
برای سازگاری با سیستمهای قدیمی است.
اگر خوب به مشکلی که در بالا برای
زیرنویسها اشاره کردیم دقت کنید، گفتیم حروف از هم جدا نشان داده میشوند و
اگر به بلوک دوم در لینکهای داده شده نگاه کنید میبینید که حروف متصل را
داراست. یعنی برای حرف س 4 حرف یا کدپوینت داراست : سـ برای کلماتی مثل سبد، ـس برای کلماتی مثل شانس، ـسـ برای کلماتی مثل بسیار، ولی خود س برای کلمات غیر متصل مثل ناس، البته بعضی حروف یک یا دو حالت میطلبند مثل د، ر که فقط دو حالت ـد و د ، ـر و ر را دارند یا مثل آ که یک حالت دارد.
من قبلا یک کلاس به نام lettersTable ایجاد کرده بودم (و دیگر نوشتن آن را ادامه ندادم) که برای هر حرف، یک آیتم در شیءایی از نوع dictionary
ساخته بودم و هر کدپوینت بلوک اول را در آن کلید و کد متقابلش را در بلوک
دوم، به صورت مقدار ذخیره کرده بودم (گفتیم که هر نماد در بلوک اول،
برابر با 4 نماد در بلوک دوم است؛ ولی ما در دیکشنری تنها مقدار اول را
ذخیره میکنیم. زیرا کد بقیه نمادها دقیقا پشت سر یکدیگر قرار گرفتهاند که
میتوان با یک جمع ساده از عدد 0 تا 3، به مقدار هر کدام از نمادها
رسید. البته ناگفته نماند بعضی نمادها 2 عدد بودند که این هم باید بررسی
شود). برای همین هر کاراکتر را با کاراکتر قبل و بعد میگرفتم و بررسی
میکردم و از یک جدول دیکشنری دیگر هم به اسم specialchars هم استفاده کردم
تا آن کاراکترهایی که تنها دو نماد یا یک نماد را دارند، بررسی کنم و این
کاراکترها همان کاراکترهایی بودند که اگر قبل یک حرف هم بیایند، حرف بعدی
به آنها نمیچسبد. برای درک بهتر، این عبارت مثال زیر را برای حرف س در
نظر بگیرید:
مستطیل = چون بین هر دو طرف س حر وجود دارد قطعا باید شکل س به صورت ـسـ انتخاب شود ، حالا مثال زیر را در نظر بگیرید:
دست = دـست که اشتباه است و باید باشد دست یعنی شکل سـ باید صدا زده شود، پس این مورد هم باید لحاظ شود.
نمونهای از کد این کلاس:
Dictionary<int ,int> letters=new Dictionary<int, int>(); //0=0x0 ,1=1x0 ,2=0x1 ,3=1x1 private void FillPrimaryTable() { //آ letters.Add(1570, 65153); //ا letters.Add(1575, 65166); //أ letters.Add(1571, 65155); //ب letters.Add(1576, 65167); //ت letters.Add(1578, 65173); //ث letters.Add(1579, 65177); //ج letters.Add(1580, 65181); ..... } Dictionary<int,byte> specialchars=new Dictionary<int, byte>(); private void SetSpecialChars() { //آ specialchars.Add(1570, 0); //ا specialchars.Add(1575, 0); //د2 specialchars.Add(1583, 1); //ذ2 specialchars.Add(1584, 1); //ر2 specialchars.Add(1585, 1); //ز2 specialchars.Add(1586, 1); //ژ specialchars.Add(1688, 1); //و2 specialchars.Add(1608, 1); //أ specialchars.Add(1571, 1); }
در آن متد هر بار یک حرف را انتخاب میکرد و حرف قبلی و بعدی آن را ارسال میکرد تا تابع CalculateIncrease آن را محاسبه کرده و کاراکتر نهایی را باز گرداند و به متغیر finalText اضافه میکرد. ولی در حین نوشتن، زمانی را به یاد آوردم که اندروید به تازگی آمده بود و هنوز در آن زمان از زبان فارسی پشتیبانی نمیکرد و حروف برنامههایی که مینوشتیم به صورت جدا از هم بود و همین مشکل را داشت که ما این مشکل را با استفاده از یک کلاس جاوا که دوست عزیزی آن را در اینجا به اشتراک گذاشته بود، حل میکردیم. پس به این صورت بود که از ادامهی نوشتن کلاس انصراف دادم و از یک کلاس دقیقتر و آماده استفاده کردم.
در واقع این کلاس همین کار بالا را با
روشی بهتر انجام میدهد. همهی نمادها به طور دقیقتری کنترل میشوند
حتی تنوینها و دیگر علائم، همه نمادها با کدهای متناظر
در یک آرایه ذخیره شدهاند که ما در بالا از نوع Dictionary استفاده کرده
بودیم.
تنها کاری که نیاز بود، باید این کد به
سی شارپ تبدیل میشد و از آنجایی که این دو زبان خیلی شبیه به هم هستند، حدود
ده دقیقهای برای ویرایش کد وقت برد که میتوانید کلاس نهایی را از اینجا دریافت کنید.
پس خط زیر در متد ToUnicode کار تبدیل اصلی را صورت میدهد:
PersianReshape reshaper = new PersianReshape(); text = reshaper.reshape(text);
//3.reverse tags text = ReverseText(text);
string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); text = ""; foreach (string line in splitedlines) { //3.reverse tags text += ReverseText(reshaper.reshape(line))+Environment.NewLine ; }
این دو تابع برای معکوس کردن عادی یک رشته به کار میروند:
ولی این تابع ReverseText جمعی از عملیات معکوس سازی ویژهی ماست؛ مرحله اول، مرحله دریافت و ذخیرهی حروف خاص در ابتدای رشته به اسم پیشوند prefix است:
مرحلهی دوم هم دریافت و ذخیرهی حروف خاص در انتهای رشته به اسم پسوند postfix است که به این تابع اضافه میکنیم:
مرحلهی سوم عملیات معکوس سازی روی رشته است و سپس با استفاده از یک Regular
Expression حروف انگلیسی و اعداد بین حروف فارسی را یافته و یک معکوس سازی
هم روی آنها انجام میدهیم تا به حالت اولشان برگردند. کل عملیات معکوس سازی
در اینجا به پایان میرسد:
تعریف عبارت با قاعدهی بالا به اسم unTargetedLetters:
آخر سر هم رشته را بهعلاوه پیشوند و پسوند جدا شده بر میگردانیم:
کد کامل تابع بدین شکل در میآید:
private string Reverse(string text) { return Reverse(text,0,text.Length); } private string Reverse(string text,int start,int end) { if (end < start) return text; string reverseText = ""; for (int i = end-1; i >=start; i--) { reverseText += text[i]; } return reverseText; }
private string ReverseText(string text) { char[] chararray = text.ToCharArray(); string reverseText = ""; bool prefixcomp = false; bool postfixcomp = false; string prefix = ""; string postfix = ""; #region get prefix symbols for (int i = 0; i < chararray.Length; i++) { if (!prefixcomp) { char ch =(char) chararray.GetValue(i) ; if (ch< 130) { prefix += chararray.GetValue(i); } else { prefixcomp = true; break; } } } #endregion }
#region get postfix symbols for (int i = chararray.Length - 1; i >-1 ; i--) { if (!postfixcomp && prefix.Length!=text.Length) { char ch = (char)chararray.GetValue(i); if (ch < 130) { postfix += chararray.GetValue(i); } else { postfixcomp = true; break; } } } #endregion
#region reverse text reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length); reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m) { return Reverse(m.Value); }); #endregion
private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled);
return prefix+ reverseText+postfix;
private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled); private string ReverseText(string text) { char[] chararray = text.ToCharArray(); string reverseText = ""; bool prefixcomp = false; bool postfixcomp = false; string prefix = ""; string postfix = ""; #region get prefix symbols for (int i = 0; i < chararray.Length; i++) { if (!prefixcomp) { char ch =(char) chararray.GetValue(i) ; if (ch< 130) { prefix += chararray.GetValue(i); } else { prefixcomp = true; break; } } } #endregion #region get postfix symbols for (int i = chararray.Length - 1; i >-1 ; i--) { if (!postfixcomp && prefix.Length!=text.Length) { char ch = (char)chararray.GetValue(i); if (ch < 130) { postfix += chararray.GetValue(i); } else { postfixcomp = true; break; } } } #endregion #region reverse text reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length); reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m) { return Reverse(m.Value); }); #endregion return prefix+ reverseText+postfix; }
در نهایت، خط آخر دلیگت همه چیز را طبق
فرمت یک دیالوگ srt چینش کرده و بر میگردانیم.
return string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value, m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ;
نمایی از برنامهی نهایی
اجرای زیرنویس تبدیل شده روی کامپیوتر
روی پلیر یا تلویزیون
نکتهی نهایی: هنگام تست زیرنویس روی فیلم متوجه شدم پلیر خطوط بلند را که در صفحهی نمایش جا نمیشود، میشکند و به دو خط تقسیم میکند. ولی نکتهی خنده دار اینجا بود که خط اول را پایین میاندازد و خط دوم را بالا. برای همین این تکه کد را نوشتم و به طور جداگانه در گیت هاب هم قرار دادهام.
این تکه کد را هم بعد از
به برنامه اضافه میکنیم:
روی پلیر یا تلویزیون
نکتهی نهایی: هنگام تست زیرنویس روی فیلم متوجه شدم پلیر خطوط بلند را که در صفحهی نمایش جا نمیشود، میشکند و به دو خط تقسیم میکند. ولی نکتهی خنده دار اینجا بود که خط اول را پایین میاندازد و خط دوم را بالا. برای همین این تکه کد را نوشتم و به طور جداگانه در گیت هاب هم قرار دادهام.
این تکه کد را هم بعد از
//1.remove tags text = CleanScriptTags(text);
text =StringUtils.ConvertToMultiLine(text);
کد متد ConvertToMultiline:
namespace Utils { public static class StringUtils { public static string ConvertToMultiLine(String text, int min = 30, int max = 40) { if (text.Trim() == "") return text; string[] words = text.Split(new string[] { " " }, StringSplitOptions.None); string text1 = ""; string text2 = ""; foreach (string w in words) { if (text1.Length < min) { if (text1.Length == 0) { text1 = w; continue; } if (w.Length + text1.Length <= max) text1 += " " + w; } else text2 += w + " "; } text1 = text1.Trim(); text2 = text2.Trim(); if (text2.Length > 0) { text1 += Environment.NewLine + ConvertToMultiLine(text2, min, max); } return text1; } } }
برنامه مورد نظر را به طور کامل میتوانید از اینجا یا اینجا به صورت فایل نهایی و هم سورس دریافت کنید.
مثالهای کامل برای پروژههای WebForm و MVC به سورس گیت هاب اضافه شد
در این مقاله با دو سیستم
کنترل نسخه git و SVN آشنا شده و تفاوتهای آنها را برای تازهکاران بررسی میکنیم. ایده
اولیه نوشتن این مقاله زمانی بود که برای یک پروژهای، اعضای تیم ما دور هم
جمع شده و در مورد ابزارهای مورد استفاده بحث کردند و یک عده از گیت و
عدهای از SVN صحبت میکردند. بر این شدم که مقالهای نوشته و ابتدا به
معرفی آنها و سپس به مزایا و معایب هر کدام بپردازیم.
امروزه، استفاده از سیستمهای کنترل نسخه ( Version Control System
) رواج زیادی پیدا کرده است. این سیستمها به شما اجازه میدهند تا
تغییراتی را که در پروژه ایجاد میشوند، ضبط و ثبت کرده تا از تغییراتی که در
سطح پروژه اتفاق میافتد آگاه شوید. با ذکر یک نمونه این تعریف را باز
میکنم:
شما
به صورت تیمی در حال انجام یک پروژه هستید و باید نسبت به تغییراتی که
اعضای تیم در یک پروژه میدهند، آگاه شوید. هر برنامه نویس بعد از انجام
تغییرات باید این تغییرات را در سیستم کنترل نسخه به روز کند تا بتوان به
سوالات زیر پاسخ داد:
آیا اگر در
بین راه به مشکل برخوردید میتوانید پروژه خود را به یک یا چند گام عقبتر
برگردانید؟ آیا میتوانید به هر یک از اعضاء تیم دسترسیهایی را به قسمت
هایی از پروژه تعیین کنید؟ میتوانید تفاوت فایلهای تغییر یافته را
بیابید؟ آیا میتوان خطاهای یک برنامه را گزارش داد و به بحث در مورد آن پرداخت؟
چه کسی کدها را تغییر داده است؟ روند کار و تغییرات به چه صورت است؟ (این
مورد برای به روز کردن نمودارهای burndown در توسعه چابک میتواند بسیار مفید باشد.)
پی نوشت: نه تنها در یک تیم بلکه بهتر هست در یک کار انفرادی هم از این سیستمها استفاده کرد تا حداقل بازبینی روی پروژههای شخصی خود هم داشته باشیم.
سیستم
کنترل گیت: این سیستم در سال 2005 توسط لینوس توروالدز خالق لینوکس معرفی
شد و از آن زمان تاکنون یکی از پر استفادهترین سیستمهای کنترل نسخه شناخته
شده است. ویکی پدیا گیت را به این شکل تعریف میکند: «یک سیستم بازبینی توزیع شده با تاکید بر جامعیت دادهها، سرعت و پشتیبانی جهت توزیع کار.»
از معروفترین سیستمهای هاستینگ که از گیت استفاده میکنند، میتوان به گیت هاب اشاره کرد.
اکثر سیستمهای هاستینگ گیت، دو حالت را ارائه میدهند:
عمومی : در
این حالت کدهای شما به عموم بازدیدکنندگان نمایش داده میشود و دیگران هم
میتوانند در تکمیل و ویرایش کدهای شما مشارکت کنند و این امکان به صورت
رایگان فراهم است. سیستم گیت هاب به دلیل محبوبیت زیادی که دارد، در اکثر
اوقات انتخاب اول همه کاربران است.
خصوصی:
در این حالت کد متعلق به شما، یا شرکت یا تیم نرم افزاری شماست و غیر از
افراد تعیین شده، شخص دیگری به کدهای شما دسترسی ندارد. اکثر سیستمهای
مدیریتی این مورد را به صورت premium پشتیبانی میکنند. به این معنا که
باید اجاره آن را به طور ماهانه پرداخت کنید. سیستم گیت هاب ماهی پنج دلار
بابت آن دریافت میکند. سیستم دیگری که در این زمینه محبوبیت دارد سیستم BitBucket هست
که که اگر تیم شما کوچک است و در نهایت پنج نفر هستید، میتوانید از حالت
خصوصی به طور رایگان استفاده کنید ولی اگر اعضای تیم شما بیشتر شد، باید
هزینهب اجاره آن را که از 10 دلار آغاز میگردد، به طور ماهیانه پرداخت
کنید.
پی نوشت: میتوانید از سیستمهای متن باز رایگان هم که قابل نصب بر روی هاست ها هم هستند استفاده کنید که در این حالت تنها هزینه هاست یا سرور برای شما میماند.
در سیستم گیت اصطلاحات زیادی وجود دارد:
Repository یا مخزن: برای هر
پروژهای که ایجاد میشود، ابتدا یک مخزن ایجاد شده و کدها داخل آن قرار
میگیرند. کاربرانی که قصد تغییر پروژه را دارند باید یک مخزن جداگانه ایجاد
کنند تا بعدا تمامی تغییرات آنها را روی پروژهی اصلی اعمال کنند.
Fork:
هر کاربری که قصد تغییر را بر روی سورس کدی، داشته باشد، ابتدا باید پروژهی نویسنده اصلی پروژه را به یک مخزنی که متعلق به خودش هست انتقال دهد. به این
عمل Fork کردن میگویند. حال کاربر تغییرات خودش را اعمال کرده و لازم هست
که این تغییرات با پروژهی اصلی که به آن Master میگوییم ادغام شوند. بدین جهت
کاربر فرمان pull request را میدهد تا به نویسندهی اصلی پروژه این موضوع
اطلاع داده شود و نویسندهی اصلی در صورت صلاحدید خود آن را تایید کند.
Branching یا شاخه بندی: نویسندهی مخزن اصلی میتواند با مفهومی با نام شاخه بندی کار کند. او با استفاده از
این مفهوم، پروژه را به قسمت یا شاخههای مختلف تقسیم کرده و همچنین با
ایجاد دسترسیهای مختلف به کاربران اجازه تغییرات را بدهد. به عنوان مثال
بخشهای مختلف پروژه از قبیل بخش منطق برنامه، داده ها، رابط کاربری و ...
میتواند باشد. بعد از انجام تغییرات روی یک شاخه میتوانید درخواست merge
ادغام شدن یا کل پروژه را داشته باشید. در عمل شاخه بندی، هیچ کدام از
شاخههای بر روی یک دیگر تاثیر یا دخالتی ندارند و حتی میتوانید چند شاخه
را جدا از بخش master با یکدیگر ادغام کنید.
به غیر از ارتباط خط فرمانی که میتوان با گیت هاب برقرار کرد، میتوان از یک سری ابزار گرافیکی خارجی هم جهت ایجاد این ارتباط، استفاده کرد:
GitHub For Windows :
نسخهی رسمی است که از طرف خود گیت هاب تهیه گردیده است و استفاده از آن
بسیار راحت است. البته یک مشکل کوچک در دانلود آن وجود دارد که دانلود آن
از طریق یک برنامهی جداگانه صورت گرفته و اصلا سرعت خوبی جهت دانلود ندارد.
Visual Studio .Net : (+ ) خود
ویژوال استودیو شامل سیستمی به اسم Microsoft Git Provider است که در بخش
تنظیمات میتوانید آن را فعال کنید (به طور پیش فرض فعال است) و به هر نوع
سیستم گیتی میتوانید متصل شوید. تنها لازم است که آدرس Url گیت را وارد
کنید.
SourceTree:
از آن دست برنامههای محبوبی است که استفاده آسانی دارد و خودم به شخصه از
آن استفاده میکنم. شامل دو نسخهی ویندوز و مک است و میتوانید با چندین
سیستم گیت مثل «گیت هاب» و «بیت باکت» که در بالا به آنها اشاره شد، به طور
همزمان کار کند.
برای یکی از پروژهها نیاز به یک آپلودر داشتم که قابلیت Drag&Drop را نیز
داشته باشد و در ضمن پیاده سازی آسانی هم داشته باشد. در این بین به
تعدادی از کتابخانههای جی کوئری میپردازیم.
FileDrop
اولین کتابخانهای که با آن آشنا شدم و از آن استفاده کردم، کتابخانهی FileDrop است که بسیار ساده و در عین حال قابلیتهای خوبی را میدهد و از فناوری Filereader (+) در Html5 برای اینکار استفاده میکند. مرورگرهای کروم، فایرفاکس 3.6 به بعد، IE10 به بعد و Opera 12 به بعد از آن پشتیبانی میکنند.
فایلهای مورد نیاز را از اینجا دانلود کنید . فایل اسکریت آن را ابتدا صدا بزنید:
<script src="~/scripts/jquery.filedrop.js" type="text/javascript"></script>
<div id="dropZone">فایل برنامه را به داخل این کادر بکشانید</div> <br> فایل یا فایلهای آپلود شده: <ul id="uploadResult"></ul>
تگ اول، محلی است که فایلها به سمت آن درگ و روی آن دراپ میشوند که از این
به بعد به آن محل آپلود میگوییم. المان بعدی جهت گزارش فایلهایی است که
آپلود شدهاند. با آپلود شدن هر تعداد فایل، اسم آن به لیست اضافه
میگردد.
کدهای css زیر را هم به صفحه اضافه کنید تا محل آپلود زیباتر شود:
.files { min-height: 42px; background: #CCC none repeat scroll 0% 0%; border-top: 1px solid #FFF; margin: 11px 0px; padding: 11px 13px; border-radius: 6px; } #dropZone.mouse-over { background-color: #1d4257; }
کد جی کوئری زیر را به صفحه اضافه کنید:
$('#dropZone').filedrop({ url: uploadAddress, paramname: 'files', maxFiles: 1, dragOver: function() { $('#dropZone').addClass('mouse-over'); }, dragLeave: function() { $('#dropZone').removeClass('mouse-over'); }, drop: function() { $('#dropZone').removeClass('mouse-over'); }, afterAll: function() { $('#dropZone').html('آپلود با موفقیت انجام شد'); }, uploadFinished: function(i, file, response, time) { $('#uploadResult').append('<li>' + file.name + '</li>'); } });
Url | آدرسی که قرار است فایلها به آن سمت ارسال شوند. |
Paramname | در سمت سرور باید فایلها را با استفاده از این نام پارامتر دریافت کنید. |
maxFiles | تعداد فایلهایی که میتوان با درگ و دراپ کردن روی آن به دست آورد. در بالا به یک فایل محدود شده است. |
dragOver | این رویداد زمانی اجرا خواهد شد که اشاره گر با حالت درگ کرده فایلها را به محل آپلود آورده است. |
dragLeave | موقعی که ماوس از محل آپلود خارج میشود |
drop | موقعی که شما فایلها را روی محل آپلود رها میکنید. |
afterAll | بعد از اینکه همه کارها تمام شد اجرا میشود.(آخرین رویداد) |
uploadFinished | کار آپلود به پایان رسیده است. در مثال بالا پس از پایان آپلود، نام فایل آپلود شده را به کاربر نشان دادهایم. |
نحوهی دریافت آن در سمت سرور, در یک اکشن متد به صورت زیر است:
[HttpPost] public virtual ActionResult UpdateApp(IEnumerable<HttpPostedFileBase>files) { foreach (HttpPostedFileBase file in files) { string filePath = Path.Combine(TempPath, file.FileName); file.SaveAs(filePath); } return Json(new {state = "success", message = "با موفقیت عملیات ارسال فایل انجام شد"}, JsonRequestBehavior.AllowGet); }
در اکشن متد بالا ما فایلها را از طریق نام پارامتر files که مشخص کرده بودیم، به عنوان یک لیست شمارشی دریافت میکنیم. کدها بالا برای سادهترین راه اندازی ممکن کفایت میکنند.
این موارد از اصلیترینها هستند که به کار میآیند. به غیر اینها یک سری خصوصیات اضافهتری هم برای آن وجود دارد.
fallback_id | اگر دوست دارید این آپلودر را نیر به یک آپلودر معمولی اتصال دهید از این شناسه استفاده کنید. |
withCredentials | با استفاده از کوکیها یک درخواست cross-origin ایجاد میکند. |
data | اگر دوست دارید به همراه فایلها اطلاعات دیگری هم به همراه آن
ارسال و پست شوند از این طریق اقدام نمایید. میتواند در قالب یک متغیر
باشد یا خروجی یک تابع.data: { param1: 'value1', param2: function(){ return calculated_data; } |
headers | برای ارسال مقدار اضافهتر در هدر درخواست به کار میرود و صدا زدن آن همانند کد data میباشد. |
error | در صورتیکه در فرایند آپلود خطایی رخ دهد، اجرا میگردد. نحوهی کدنویسی آن و بررسی خطاهای آن به شرح زیر است:error: function(err, file) { switch(err) { case 'BrowserNotSupported': alert('مرورگر از این فناوری پشتیبانی نمیکند') break; case 'TooManyFiles': // قصد آپلود همزمان فایلهای بیشتری از حد مجاز تعیین شده دارید break; case 'FileTooLarge': //حداقل حجم یکی از فایلها از حجم مجاز تعیین شده بیشتر است //برای دسترسی به نام آن فایل از کد زیر استفاده کنید //file.name break; case 'FileTypeNotAllowed': // نوع حداقل یکی از فایلها با نوعها مشخص شده ما یکی نیست break; case 'FileExtensionNotAllowed': // پسوند حداقل یکی از فایلها مورد تایید نیست break; default: break; } } |
allowedfiletypes | نوع فایلهای مجاز را تعیین میکند:allowedfiletypes: |
allowedfileextensions | پسوند فایل هایی که برای آپلود مجاز هستند را معرفی میکند.allowedfileextensions: |
maxfilesize | حداکثر حجم مجاز برای هر فایل که به مگابایت بیان میشود. |
docOver | این رویداد زمانی اجرا میشود که فایلهای درگ شده شما وارد محیط یا پنجره مرورگر میشود. |
uploadStarted | این رویداد زمانی اجرا میگردد که فرایند آپلود هر فایل به طور جداگانه در حال آغاز شدن است: متغیر i در کد زیر شامل اندیس فایلی است که آپلودش آغاز شده است و این اندیس از صفر آغاز میشود. متغیر file دسترسی شما را به اطلاعات یک فایل باز میکند مانند نام فایل. متغیر len تعداد فایل هایی را که کاربر در محل آپلود رها کرده است، باز میگرداند. function(i, file, len){ }, |
uploadFinished | با اتمام آپلود هر فایل، این رویداد فراخوانی میگردد. دو
پارامتر اول آن، همانند سابق هستند. پارامتر response خروجی json ایی را که در سمت
سرور برگرداندیم، به ما باز میگرداند. پارامتر بعدی، زمانی را که برای
آپلود طول کشیده است، بر میگرداند. function(i, file, response, time) { } |
progressUpdated | این رویداد برای نمایش پیشرفت یک آپلود مناسب است که آخرین پارامتر آن یک عدد صحیح از پیشرفت فایل را بر میگرداند.function(i, file, progress) { }, |
globalProgressUpdated | این رویداد میزان پیشرفت کلیه فایلها را به درصد باز میگرداند:function(progress) { $('#progress div') |
speedUpdated | سرعت آپلود هر فایل را با کیلوبیت بر ثانیه مشخص میکند.function(i, file, speed) { } |
rename | در صورتی که قصد تغییر نام فایل ارسالی را دارید میتوانید از این رویداد استفاده کنید. پارامتر name، نام اصلی فایل را بر میگرداند که میتوانید آن را دستکاری کنید و نام جدیدی را به عنوان خروجی برگردانید. نمونه کاربردی از این رویداد rename: function(name) { } |
beforeEach | این رویداد قبل از آپلود هر فایل آغاز میگردد و برگرداندن مقدار false در آن باعث جلوگیری و کنسل شدن آپلود آن فایل میگردد.function(file) { } |
beforeSend | پارامترهای اولی تکراری هستند ولی آخرین پارامتر یک
تابع done را میتوان به آن پاس کرد که قبل از اجرای کل عملیات آپلود صدا
زده میشود.function(file, i, done) { } |
رویدادی به اسم queuefiles هم هست تعداد فایلهایی را که میتوانند به طور موازی و همزمان آپلود گردند، مشخص میکند. ولی دراین حالت maxfiles مورد استفاده قرار نمیگیرد. جهت بررسی یک مثال عملی و همچنین کدهای سمت سرور در PHP میتوانید از این آموزش استفاده کنید.
با تستی که به صورت لوکال رو آن انجام دادم به نظر نمیرسد برای فایلهای با حجم متوسط به بالا مناسب باشد و برای فایلهای با حجم کم مناسب میباشد. یک فایل 8 مگابایتی در حالت لوکال 9 ثانیه آپلود آن زمان برد و برای فایلهای بزرگتر، فایرفاکس دیالوگ Stop Script را نشان داد.
PlUpload
این کتابخانه متن باز هم بسیار کارآمد و ساده و قابل انعطاف است و مثالهای آماده زیادی دارد. سایت سابسن هم در بخش آپلود زیرنویسها از این کتابخانه استفاده میکند. از آنجا که آموزش این کتابخانه در سایت جاری آمده است از ذکر نکات بیشتر در مورد آن خودداری مینماییم.
Bootstrap FileStyle
اگر از قالب بوت استراپ استفاده میکنید و دوست دارید روی المان input file قدیمی، ولی به شکلی مدرن کار کنید این کتابخانه هم فراموش نشود.
DropZoneJS
این کتابخانه به نسبت DropFile امکانات بیشتری را دارد و در سایت اختصاصی آن مثالها و مستندات خوبی قرار گرفته است. در سادهترین حالت آن ابتدا فایل کتابخانه را صدا زده و سپس تگ فرم را به آن نسبت دهید:
<script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script> <form action="/upload-target" class="dropzone"></form>
ولی اگر بخواهید آن را به سمت سرور ارسال کنید و از آنجا آن را کنترل کنید، کد فرم را به شکل زیر تغییر دهید:
ابتدا بستهی نیوگت آن را صدا بزنید:
Install-Package dropzone
با نصب این کتابخانه یک سری فایل CSS هم به سیستم اضافه میشود که میتوانید برای استایل دهی هر چه بیشتر از آن بهره ببرید. کد فرم را به شکل زیر تغییر دهید:
<form action="~/Home/SaveUploadedFile" method="post" enctype="multipart/form-data" class="dropzone" id="dropzoneForm" style="width: 50px; background: none; border: none;"> <div class="fallback"> <input name="file" type="file" multiple /> <input type="submit" value="Upload" /> </div> </form>
با استفاده از کدنویسی هم میتوان یک المان را به یک آپلودر تبدیل کرد:
var myDropzone = new Dropzone("div#myId", { url: "/file/post"}); //============ OR ==================== $("div#myId").dropzone({ url: "/file/post" });
برای کانفیگ آپلودرهایی که از طریف المانهای Html ایجاد میشوند، میتوان از کد زیر استفاده کرد و یک تنظیم عمومی برای تمامی آپلودرهای html آن صفحه ایجاد کرد.
Dropzone.options.myId= { paramName: "file", //نام پارامتری که فایل از طریق آن انتقال میبابد maxFilesize: 2, // MB accept: function(file, done) { if (file.name == "justinbieber.jpg") { done("Naha, you don't."); } else { done(); } } };
ازآنجا که این کتابخانه از تنظیمات وسیعی استفاده میکند و از حوصلهی این مقاله خارج است، بهتر هست که صفحهی مستندات آن را که کامل هم هست، مطالعه بفرمایید. از سری قابلیتهایی که پشتیبانی میکند: موارد پوشش داده شده در FileDrop، ساخت layout، ایجاد صف، متد حذف و اضافه و از این قبیل، ایجاد تصویر تمبر مانند و ...
یک نکته تکمیلی در مورد آپلود: در ASP.net به طور پیش فرض نهایت حجم فایل آپلودی 4 مگابایتی تعیین شده است که میتوانید آن را از طریق web.config تغییر دهید:
<configuration> <system.web> <httpRuntime maxRequestLength="1048576" /> </system.web> </configuration>
<system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="1073741824" /> </requestFiltering> </security> </system.webServer>
در هر دو کد بالا نهایت حجم بر روی یک گیگابایت تعیین شده است که maxRequestLength به صورت کیلوبایت و maxAllowContentLength به صورت بایت تعیین شده است. توصیه میشود هر دو شکل آن را وارد کنید. به خصوص که IIS Express از کد ابتدایی استفاده میکند و بخواهید نتیجهی آن را در تستها ببینید.