$('body').tooltip({ selector: '[rel=tooltip]' });
یک نکتهی تکمیلی: تنظیم پویای عنوان صفحات در برنامههای Blazor
برای تنظیم پویای عنوان یک صفحهی وب، نیاز است با DOM API مرورگر به صورت مستقیم کار کرد. برای مثال فایل wwwroot\main.js را که مدخل آن به کامپوننت Host_ و یا صفحهی index.html اضافه میشود، به صورت زیر تکمیل میکنیم:
اکنون میخواهیم این متد جاوااسکریپتی را که مستقیما با شیء document کار میکند، در کامپوننت جدید Client\Shared\PageTitle.razor استفاده کنیم:
در اینجا کامپوننت جدیدی تعریف شدهاست که به محض تنظیم مقدار پارامتر عنوان آن، سبب فراخوانی متد جاوا اسکریپتی blazorSetTitle میشود. برای نمونه روش استفادهی از آن در کامپوننت Counter، جهت نمایش عنوانی پویا، به محض تغییر مقدار شمارشگر، به صورت زیر میتواند باشد:
این روش در برنامههای Blazor Server کار نخواهد کرد و در حین فراخوانی متد InvokeVoidAsync یک NullReferenceException مشاهده میشود؛ چون این نوع برنامههای Blazor Server به همراه یک مرحلهی pre-render در سمت سرور هستند که ابتدا، کار تهیهی HTML ای را که باید به سمت مرورگر ارسال کنند، به پایان میرسانند. در این مرحله خبری از DOM نیست که بتوان به آن دسترسی یافت و تغییری را در آن ایجاد کرد.
برای رفع این مشکل همانطور که در مطلب جاری نیز عنوان شد، باید از روال رویدادگردان OnAfterRenderAsync استفاده کرد. در این حالت کدهای کامپوننت PageTitle.razor به صورت زیر تغییر میکنند:
روال رویدادگردان OnAfterRenderAsync پس از اینکه کار بارگذاری و تشکیل کامل DOM در مرورگر انجام شد، فراخوانی میشود. به همین جهت دیگر دسترسی به شیء document.title، سبب بروز یک NullReferenceException نخواهد شد.
یک نکته: قرار است در Blazor 6x، کامپوننتهای جدید Title، Link و Meta جهت تنظیم اطلاعات تگ head صفحه، به صورت استاندارد اضافه شوند:
برای تنظیم پویای عنوان یک صفحهی وب، نیاز است با DOM API مرورگر به صورت مستقیم کار کرد. برای مثال فایل wwwroot\main.js را که مدخل آن به کامپوننت Host_ و یا صفحهی index.html اضافه میشود، به صورت زیر تکمیل میکنیم:
window.JsFunctionHelper = { blazorSetTitle: function (title) { document.title = title; } };
@inject IJSRuntime JSRuntime @code { [Parameter] public string Title { get; set; } protected override async Task OnParametersSetAsync() { await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title); } }
@page "/counter" <PageTitle Title="@GetPageTitle()" /> <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } private string GetPageTitle() => $"Counter ({currentCount})"; }
برای رفع این مشکل همانطور که در مطلب جاری نیز عنوان شد، باید از روال رویدادگردان OnAfterRenderAsync استفاده کرد. در این حالت کدهای کامپوننت PageTitle.razor به صورت زیر تغییر میکنند:
@inject IJSRuntime JSRuntime @code { [Parameter] public string Title { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title); } }
یک نکته: قرار است در Blazor 6x، کامپوننتهای جدید Title، Link و Meta جهت تنظیم اطلاعات تگ head صفحه، به صورت استاندارد اضافه شوند:
<Title Value="@title" /> <Meta name="description" content="Modifying the head from a Blazor component." /> <Link href="main.css" rel="stylesheet" />
اگر جدیدا قصد برنامه نویسی اندروید را کردهاید، یا هنوز روشهای متدوالی را برای
کار با این زبان انتخاب نکردهاید؛ به نظرم این مقاله میتواند کمک خوبی
برای شما باشد. مسائلی که بیان میکنم در واقع از تجربیات شخصی و راه حل
هایی است که برای خودم تعیین کردهام و تعدادی از آنها را در طول مدتی که
در این زمینه فعالیت کردهام، از جاهای مختلف دیده و در یک جا گردآوری
کردهام. برای نامگذاری اشیاء و متغیرها و دیگر موارد، من از این قاعده پیروی میکنم که به نظرم بسیار ایده آل میباشد. الگوی معماری هم که جدیدا مورد استفاده قرار دادهام، الگوی 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(); }
مطالب
EF Code First #7
مدیریت روابط بین جداول در EF Code first به کمک Fluent API
EF Code first بجای اتلاف وقت شما با نوشتن فایلهای XML تهیه نگاشتها یا تنظیم آنها با کد، رویه Convention over configuration را پیشنهاد میدهد. همین رویه، جهت مدیریت روابط بین جداول نیز برقرار است. روابط one-to-one، one-to-many، many-to-many و موارد دیگر را بدون یک سطر تنظیم اضافی، صرفا بر اساس یک سری قراردادهای توکار میتواند تشخیص داده و اعمال کند. عموما زمانی نیاز به تنظیمات دستی وجود خواهد داشت که قراردادهای توکار رعایت نشوند و یا برای مثال قرار است با یک بانک اطلاعاتی قدیمی از پیش موجود کار کنیم.
مفاهیمی به نامهای Principal و Dependent
در EF Code first از یک سری واژههای خاص جهت بیان ابتدا و انتهای روابط استفاده شده است که عدم آشنایی با آنها درک خطاهای حاصل را مشکل میکند:
الف) Principal : طرفی از رابطه است که ابتدا در بانک اطلاعاتی ذخیره خواهد شد.
ب) Dependent : طرفی از رابطه است که پس از ثبت Principal در بانک اطلاعاتی ذخیره میشود.
Principal میتواند بدون نیاز به Dependent وجود داشته باشد. وجود Dependent بدون Principal ممکن نیست زیرا ارتباط بین این دو توسط یک کلید خارجی تعریف میشود.
کدهای مثال مدیریت روابط بین جداول
در دنیای واقعی، همهی مثالها به مدل بلاگ و مطالب آن ختم نمیشوند. به همین جهت نیاز است یک مدل نسبتا پیچیدهتر را در اینجا بررسی کنیم. در ادامه کدهای کامل مثال جاری را مشاهده خواهید کرد:
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
public int Id { set; get; }
public string FirstName { set; get; }
public string LastName { set; get; }
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
public virtual ICollection<CustomerAlias> Aliases { get; set; }
public virtual ICollection<Role> Roles { get; set; }
public virtual Address Address { get; set; }
}
}
namespace EF_Sample35.Models
{
public class CustomerAlias
{
public int Id { get; set; }
public string Aka { get; set; }
public virtual Customer Customer { get; set; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Role
{
public int Id { set; get; }
public string Name { set; get; }
public virtual ICollection<Customer> Customers { set; get; }
}
}
namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
public int Id { get; set; }
public bool LikesPasta { get; set; }
public bool LikesPizza { get; set; }
public int AverageDailyCalories { get; set; }
public virtual Customer Customer { get; set; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
public virtual ICollection<Customer> Customers { set; get; }
}
}
همچنین تعاریف نگاشتهای برنامه نیز مطابق کدهای زیر است:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}
به همراه Context زیر:
using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample35.Mappings;
using EF_Sample35.Models;
namespace EF_Sample35.DataLayer
{
public class Sample35Context : DbContext
{
public DbSet<AlimentaryHabits> AlimentaryHabits { set; get; }
public DbSet<Customer> Customers { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new CustomerConfig());
modelBuilder.Configurations.Add(new CustomerAliasConfig());
base.OnModelCreating(modelBuilder);
}
}
public class Configuration : DbMigrationsConfiguration<Sample35Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample35Context context)
{
base.Seed(context);
}
}
}
که نهایتا منجر به تولید چنین ساختاری در بانک اطلاعاتی میگردد:
توضیحات کامل کدهای فوق:
تنظیمات روابط one-to-one و یا one-to-zero
زمانیکه رابطهای 0..1 و یا 1..1 است، مطابق قراردادهای توکار EF Code first تنها کافی است یک navigation property را که بیانگر ارجاعی است به شیء دیگر، تعریف کنیم (در هر دو طرف رابطه).
برای مثال در مدلهای فوق یک مشتری که در حین ثبت اطلاعات اصلی او، «ممکن است» اطلاعات جانبی دیگری (AlimentaryHabits) نیز از او تنها در طی یک رکورد، دریافت شود. قصد هم نداریم یک ComplexType را تعریف کنیم. نیاز است جدول AlimentaryHabits جداگانه وجود داشته باشد.
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual AlimentaryHabits AlimentaryHabits { set; get; }
}
}
namespace EF_Sample35.Models
{
public class AlimentaryHabits
{
// ...
public virtual Customer Customer { get; set; }
}
}
در اینجا خواص virtual تعریف شده در دو طرف رابطه، به EF خواهد گفت که رابطهای، 1:1 برقرار است. در این حالت اگر برنامه را اجرا کنیم، به خطای زیر برخواهیم خورد:
Unable to determine the principal end of an association between
the types 'EF_Sample35.Models.Customer' and 'EF_Sample35.Models.AlimentaryHabits'.
The principal end of this association must be explicitly configured using either
the relationship fluent API or data annotations.
EF تشخیص داده است که رابطه 1:1 برقرار است؛ اما با قاطعیت نمیتواند طرف Principal را تعیین کند. بنابراین باید اندکی به او کمک کرد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// one-to-one
this.HasOptional(x => x.AlimentaryHabits)
.WithRequired(x => x.Customer)
.WillCascadeOnDelete();
}
}
}
همانطور که ملاحظه میکنید در اینجا توسط متد WithRequired طرف Principal و توسط متد HasOptional، طرف Dependent تعیین شده است. به این ترتیب EF میتوان یک رابطه 1:1 را تشکیل دهید.
توسط متد WillCascadeOnDelete هم مشخص میکنیم که اگر Principal حذف شد، لطفا Dependent را به صورت خودکار حذف کن.
توضیحات ساختار جداول تشکیل شده:
هر دو جدول با همان خواص اصلی که در دو کلاس وجود دارند، تشکیل شدهاند.
فیلد Id جدول AlimentaryHabits اینبار دیگر Identity نیست. اگر به تعریف قید FK_AlimentaryHabits_Customers_Id دقت کنیم، در اینجا مشخص است که فیلد Id جدول AlimentaryHabits، به فیلد Id جدول مشتریها متصل شده است (یعنی در آن واحد هم primary key است و هم foreign key). به همین جهت به این روش one-to-one association with shared primary key هم گفته میشود (کلید اصلی جدول مشتری با جدول AlimentaryHabits به اشتراک گذاشته شده است).
تنظیمات روابط one-to-many
برای مثال همان مشتری فوق را درنظر بگیرید که دارای تعدادی نام مستعار است:
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<CustomerAlias> Aliases { get; set; }
}
}
namespace EF_Sample35.Models
{
public class CustomerAlias
{
// ...
public virtual Customer Customer { get; set; }
}
}
همین میزان تنظیم کفایت میکند و نیازی به استفاده از Fluent API برای معرفی روابط نیست.
در طرف Principal، یک مجموعه یا لیستی از Dependent وجود دارد. در Dependent هم یک navigation property معرف طرف Principal اضافه شده است.
جدول CustomerAlias اضافه شده، توسط یک کلید خارجی به جدول مشتری مرتبط میشود.
سؤال: اگر در اینجا نیز بخواهیم CascadeOnDelete را اعمال کنیم، چه باید کرد؟
پاسخ: جهت سفارشی سازی نحوه تعاریف روابط حتما نیاز به استفاده از Fluent API به نحو زیر میباشد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerAliasConfig : EntityTypeConfiguration<CustomerAlias>
{
public CustomerAliasConfig()
{
// one-to-many
this.HasRequired(x => x.Customer)
.WithMany(x => x.Aliases)
.WillCascadeOnDelete();
}
}
}
اینکار را باید در کلاس تنظیمات CustomerAlias انجام داد تا بتوان Principal را توسط متد HasRequired به Customer و سپس dependent را به کمک متد WithMany مشخص کرد. در ادامه میتوان متد WillCascadeOnDelete یا هر تنظیم سفارشی دیگری را نیز اعمال نمود.
متد HasRequired سبب خواهد شد فیلد Customer_Id، به صورت not null در سمت بانک اطلاعاتی تعریف شود؛ متد HasOptional عکس آن است.
تنظیمات روابط many-to-many
برای تنظیم روابط many-to-many تنها کافی است دو سر رابطه ارجاعاتی را به یکدیگر توسط یک لیست یا مجموعه داشته باشند:
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Role
{
// ...
public virtual ICollection<Customer> Customers { set; get; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// ...
public virtual ICollection<Role> Roles { get; set; }
}
}
همانطور که مشاهده میکنید، یک مشتری میتواند چندین نقش داشته باشد و هر نقش میتواند به چندین مشتری منتسب شود.
اگر برنامه را به این ترتیب اجرا کنیم، به صورت خودکار یک رابطه many-to-many تشکیل خواهد شد (بدون نیاز به تنظیمات نگاشتهای آن). نکته جالب آن تشکیل خودکار جدول ارتباط دهنده واسط یا اصطلاحا join-table میباشد:
CREATE TABLE [dbo].[RolesJoinCustomers](
[RoleId] [int] NOT NULL,
[CustomerId] [int] NOT NULL,
)
سؤال: نامهای خودکار استفاده شده را میخواهیم تغییر دهیم. چکار باید کرد؟
پاسخ: اگر بانک اطلاعاتی برای بار اول است که توسط این روش تولید میشود شاید این پیش فرضها اهمیتی نداشته باشد و نسبتا هم مناسب هستند. اما اگر قرار باشد از یک بانک اطلاعاتی موجود که امکان تغییر نام فیلدها و جداول آن وجود ندارد استفاده کنیم، نیاز به سفارشی سازی تعاریف نگاشتها به کمک Fluent API خواهیم داشت:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-many
this.HasMany(p => p.Roles)
.WithMany(t => t.Customers)
.Map(mc =>
{
mc.ToTable("RolesJoinCustomers");
mc.MapLeftKey("RoleId");
mc.MapRightKey("CustomerId");
});
}
}
}
تنظیمات روابط many-to-one
در تکمیل مدلهای مثال جاری، به دو کلاس زیر خواهیم رسید. در اینجا تنها در کلاس مشتری است که ارجاعی به کلاس آدرس او وجود دارد. در کلاس آدرس، یک navigation property همانند حالت 1:1 تعریف نشده است:
namespace EF_Sample35.Models
{
public class Address
{
public int Id { set; get; }
public string City { set; get; }
public string StreetAddress { set; get; }
public string PostalCode { set; get; }
}
}
using System.Collections.Generic;
namespace EF_Sample35.Models
{
public class Customer
{
// …
public virtual Address Address { get; set; }
}
}
این رابطه توسط EF Code first به صورت خودکار به یک رابطه many-to-one تفسیر خواهد شد و نیازی به تنظیمات خاصی ندارد.
زمانیکه جداول برنامه تشکیل شوند، جدول Addresses موجودیتی مستقل خواهد داشت و جدول مشتری با یک فیلد به نام Address_Id به جدول آدرسها متصل میگردد. این فیلد نال پذیر است؛ به عبارتی ذکر آدرس مشتری الزامی نیست.
اگر نیاز بود این تعاریف نیز توسط Fluent API سفارشی شوند، باید خاصیت public virtual ICollection<Customer> Customers به کلاس Address نیز اضافه شود تا بتوان رابطه زیر را توسط کدهای برنامه تعریف کرد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample35.Models;
namespace EF_Sample35.Mappings
{
public class CustomerConfig : EntityTypeConfiguration<Customer>
{
public CustomerConfig()
{
// many-to-one
this.HasOptional(x => x.Address)
.WithMany(x => x.Customers)
.WillCascadeOnDelete();
}
}
}
متد HasOptional سبب میشود تا فیلد Address_Id اضافه شده به جدول مشتریها، null پذیر شود.
سلام؛
طبق مطلب ارائه شده در مقاله:
تابع submit :
value مقدار دکمه ای است که بروی آن کلیک شده ، message مقدار html تعریف شده برای state است ، formVals هم در صورتی که در html تعریف شده برای state ، المنتهای فرم وجود داشته باشد ، شامل نام/مقادیر آنها میباشد . ( برای دریافت مقادیر فرم ، باید از نام المنت استفاده نمایید . )
خب حالا مشابه مثال زیر عمل کنید:
طبق مطلب ارائه شده در مقاله:
تابع submit :
function(event, value, message, formVals){}
خب حالا مشابه مثال زیر عمل کنید:
<div class="prompt-content" style="display: none;"> <span>ایمیل خود را وارد نمایید : </span> <span> <input type="text" name="user_email" /> </span> </div>
$.prompt( $(".prompt-content").html(), { submit: function (e, v, m, f) { var userEmail = f["user_email"]; console.log(userEmail); } });
نظرات مطالب
تغییر اندازه تصاویر #1
بله دیدم جالب بود واسم، اما تغییر اندازه ندیدم کدوم متده؟
تغییرات مورد نیاز جهت ارتقاء به ASP.NET Core 2.1
تغییرات سمت سرور:
1) ابتدا فایل csproj فوق به صورت زیر تغییر میکند:
در اینجا چون از بستهی Microsoft.AspNetCore.App استفاده میشود، دیگر نیازی به ذکر مستقیم بستهی SignalR نیست و این بسته جزئی از آن است.
2) سپس در فایل Startup.cs، به ابتدای path باید یک / اضافه شود؛ وگرنه در زمان اجرا برنامه کرش میکند:
3) تمام متدهای InvokeAsync شدهاند SendAsync:
و
تغییرات سمت کلاینت:
1) نام بستهی signalr در npm به صورت ذیل تغییر کردهاست و دیگر signalr-client نیست:
2) پس از نصب بستهی جدید signalr، مسیر فایل signalr.min.js در فایل bundleconfig.json به صورت زیر تغییر میکند:
3) متد invoke به send تغییر نام یافتهاست:
تغییرات سمت سرور:
1) ابتدا فایل csproj فوق به صورت زیر تغییر میکند:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.1</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-preview1-final" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.1.0-preview1-final" /> <DotNetCliToolReference Include="BundlerMinifier.Core" Version="2.6.362" /> </ItemGroup> <Target Name="PrecompileScript" BeforeTargets="BeforeBuild"> <Exec Command="dotnet bundle" /> </Target> </Project>
2) سپس در فایل Startup.cs، به ابتدای path باید یک / اضافه شود؛ وگرنه در زمان اجرا برنامه کرش میکند:
app.UseSignalR(routes => { routes.MapHub<MessageHub>(path: "/message"); });
await _messageHubContext.Clients.All.SendAsync("broadcastMessage", message);
return Clients.All.SendAsync("broadcastMessage", message);
تغییرات سمت کلاینت:
1) نام بستهی signalr در npm به صورت ذیل تغییر کردهاست و دیگر signalr-client نیست:
npm install @aspnet/signalr
node_modules/@aspnet/signalr/dist/browser/signalr.min.js
connection.send('send', $('#message').val());
rel مخفف release هست. rel در اینجا اضافی بکار برده شده چون معنای RTM Rel = release to manufacturing release را میدهد.
یک نکتهی تکمیلی
پیاده سازی مطلب جاری برای ASP.NET Identity 2.x یک چنین تغییراتی را پیدا میکند:
با این متد بررسی درخواستها:
پیاده سازی مطلب جاری برای ASP.NET Identity 2.x یک چنین تغییراتی را پیدا میکند:
app.UseCookieAuthentication(new CookieAuthenticationOptions { // ... Provider = new CookieAuthenticationProvider { OnValidateIdentity = context => { if(shouldIgnoreRequest(context)) // How to ignore Authentication Validations for static files in ASP.NET Identity { return Task.FromResult(0); } return container.GetInstance<IApplicationUserManager>().OnValidateIdentity().Invoke(context); } }, // ... });
private static bool shouldIgnoreRequest(CookieValidateIdentityContext context) { string[] reservedPath = { "/__browserLink", "/img", "/fonts", "/Scripts", "/Content", "/Uploads", "/Images" }; return reservedPath.Any(path => context.OwinContext.Request.Path.Value.StartsWith(path, StringComparison.OrdinalIgnoreCase)) || BundleTable.Bundles.Select(bundle => bundle.Path.TrimStart('~')).Any(bundlePath => context.OwinContext.Request.Path.Value.StartsWith(bundlePath,StringComparison.OrdinalIgnoreCase)); }
قسمتهای اول تا سوم این مقاله: + و + و +
در قسمت چهارم قصد داریم هدر مربوط به Content Expiration Date را توسط یک Http module به محتوای غیرپویای سایت مانند تصاویر ، فایلهای CSS و غیره اعمال کنیم. این روش از روش قسمت دوم سادهتر است و جامعتر.
ابتدا یک پروژهی Class library جدید را به نام StaticContentCacheModule ایجاد کرده و سپس ارجاعی را به اسمبلی استاندارد System.Web.dll به آن خواهیم افزود. سپس کدهای مرتبط با این ماژول به شرح زیر هستند:
//StaticCache .cs
using System;
using System.Web;
namespace StaticContentCacheModule
{
public class StaticCache : IHttpModule
{
public void Init(HttpApplication context)
{
context.PreSendRequestHeaders += context_PreSendRequestHeaders;
}
static void context_PreSendRequestHeaders(object sender, EventArgs e)
{
//capture the current Response
var currentResponse = ((HttpApplication)sender).Response;
if (CacheManager.ShouldCache(currentResponse.ContentType))
{
currentResponse.AddHeader("cache-control", "public");
currentResponse.AddHeader("Expires", DateTime.Now.Add(TimeSpan.FromDays(30)).ToString());
}
}
public void Dispose() { }
}
}
در اینجا ContentType تک تک عناصری که توسط وب سرور ارائه خواهند شد، بررسی میشود. اگر نیازی به کش شدن آنها وجود داشت (توسط کلاس CacheManager این امر مشخص میگردد)، هدر مربوطه اضافه میگردد.
//CacheManager.cs
using System;
namespace StaticContentCacheModule
{
class CacheManager
{
public static bool ShouldCache(string contentType)
{
contentType = contentType.ToLower();
string[] parts =
contentType.Split(
new[] { ';' },
StringSplitOptions.RemoveEmptyEntries
);
if (parts.Length > 0)
contentType = parts[0];
bool cache = false;
switch (contentType)
{
case "text/css":
cache = true; break;
case "text/javascript":
case "text/jscript":
cache = true; break;
case "image/jpeg":
cache = true; break;
case "image/gif":
cache = true; break;
case "application/octet-stream":
cache = true; break;
default:
{
if (contentType.Contains("javascript"))
cache = true;
if (contentType.Contains("css"))
cache = true;
if (contentType.Contains("image"))
cache = true;
if (contentType.Contains("application"))
cache = true;
}
break;
}
return cache;
}
}
}
در این کلاس، contentType دریافتی بررسی میشود. اگر نوع محتوای قابل ارائه از نوع CSS ، JavaScript ، تصویر و یا Application بود، یک مقدار true بازگشت داده خواهد شد.
نهایتا برای استفاده از این Http module جدید در IIS6 به قبل در وب کانفیگ برنامه خواهیم داشت:
<httpModules>
<add name="StaticContentCacheModule" type="StaticContentCacheModule.StaticCache, StaticContentCacheModule"/>
</httpModules>
و یا در IIS7 این تغییرات به صورت زیر میتواند باشد:
<system.webServer>
<modules>
<add name="StaticContentCacheModule" type="StaticContentCacheModule.StaticCache, StaticContentCacheModule"/>
</modules>
اکنون اگر یک پروژهی آزمایشی جدید ASP.Net را گشوده و فایل css سادهای را به آن اضافه کنیم، بررسی هدر نهایی توسط افزونهی YSlow به صورت زیر خواهد بود: