در ایران، اکثر برنامهنویسان پلتفرم داتنت، نرمافزارهای مبتنی بر پایگاهداده مینویسند و باز هم از آنجایی که دیتابیس SQLServer یکی از محبوبترین پایگاههای داده در ایران است، مشکل کندی این دیتابیس در سیستمهای قدیمیترو غیرسروری، کاملا محسوس است. از این رو نرمافزارها هنگام دریافت اطلاعات از سرور، دچار توقفی چند ثانیهای و یا در مواردی با چند دقیقه توقف و خارج شدن از سرویسدهی روبهرو میشوند. بیشتر کاربران این مشکل را از برنامه میدانند و با نارضایتی از این گونه برنامهها یاد میکنند، در صورتی که میتوان با پیادهسازی چندنخی، این نارضایتی را بهطور کامل از بین برد.
در برنامههای ویندوزی، اشیا و کنترلها متعلق به نخی هستند به نام UIThread و نمیتوان از نخی دیگر به اشیا این نخ دسترسی داشت:
void ShowProgress(){ /*Update UI*/ }
void CalcPi(int digits)
{ ShowProgress(); /*Calculate PI*/}
void calcButton_Click(object sender, EventArgs e) {
this.calcToolStripProgressBar.Visible = true;
this.calcToolStripStatusLabel.Text = "Calculating...";
Thread piThread = new Thread(CalcPiThreadStart);
piThread.Start(1000); }
void CalcPiThreadStart(object digits) {
CalcPi((int)digits); }
تابع CalcPi مقدار عدد PI را با 1000 رقم اعشار دقت محاسبه میکند و این عملی زمانبر است، فرض کنید کد بالا بصورت چندنخی نوشته نشده بود، آن وقت چه اتفاقی میافتاد؟ برنامه هنگ میکرد و کاربر هم شما را بهعنوان برنامه نویسی که به کار خودش وارد نیست، خطاب میکرد. متد ShowProgress یک ProgressBar را مقداردهی میکند و درصد انجام کار را نشان میدهد این متد بهطور موازی با متد CalcPi انجام میشود، بهنظر میرسد که همه چیز روبهراه است اما بعد از اینکه کد را اجرا کردید با خطای زیر مواجه میشوید:
Illegal Cross-Thread Operation Detected and InvalidOperationException Raised
این خطا به ما میگوید دسترسی غیرقانونی در تردها انجام شده است، خوب راه حل رفع این مشکل چیست؟
تیم اصلی برنامهنویسی داتنت متدی به نام Invoke را در کلاس Control (که پدر تمام کلاسهای کنترلی ویندوز فرم است) قرار دادهاند، این متد یک Delegate را به عنوان پارامتر ورودی دریافت میکند، که نشاندهنده عملی است که برای تغییر کنترلها باید از ان استفاده شود. با استفاده از این متد شما میتوانید از طریق نخی دیگر به کنترلهای دیگر برنامه دسترسی داشته باشید، برای اینکه بدانیم که چه زمانی باید از Invoke استفاده شود، مقدار ویژگی InvokeRequired را بررسی میکنیم. این ویژگی به ما نشان میدهد که آیا بهتر است از Invoke استفاده شود، یا اینکه طور مستقیم میتوان کنترلهای برنامه را تغییر داد، حالا کد بالا را با استفاده از مطالب گفته شده باز نویسی میکنیم.
delegate void ShowProgressCallback();
void CalcPi(int digits) {
ShowProgressCallback showprogress=
new ShowProgressCallback(ShowProgress);
if (this.InvokeRequired)
this.Invoke(showprogress);
else
this.ShowProgress();}
اگر کد بالا را اجرا کنید دیگر با پیغام خطایی که در بالا گفته شد روبهرو نمیشوید، علت چیست؟
چندنخی در WPF
در WPF نیز مثل ویندوز فرم عمل میشود ولی با یک سری تفاوتها که در زیر آنها را بیان میکنیم، همانطور که گفته شد، در برنامههای ویندوزی کنترل اشیا در اختیار تردی است که آن را بهوجود آورده و امکان تغییر آن با همان نخ است. همچنین گفتیم که از متد Invoke برای حل این مشکل استفاده میکنیم. در WPF، از شئی Dispatcher استفاده میشود. در WPF اشیایی که با چند نخی در ارتباط هستند، از کلاس DispatcherObject مشتق شدهاند، از آن جمله میتوان به DependencyObject اشاره کرد، که از کلاسهای پایه در WPF است، بنابراین میتوان گفت، تمام کنترلهای UIElement به چندنخی وابستهاند.
همانطور که گفته شد برای حل مشکل دسترسی از یک نخ به نخ دیگر باید از Dispatcher استفاده کرد؛ شئی DispatcherObject یک ویژگی بهنام Dispatcher دارد که به System.Windows.Threading.Diapatcher اشاره میکند. توسط متد Invoke این شئی شما میتوانید Delegate مربوطه به عملیات پسزمینه را در صف Dispatcher قرار دهید که در UIThread فراخوانی شود، این کار را مانند زیر انجام میدهیم.
متد Invoke چهار بازنویسی(Overload) دارد که ما یکی از آنها را اینجا توضیح خواهیم داد و بقیه را بر عهده خودتان میگذاریم. این متد پارامترهای زیر را بهعنوان آرگومان میگیرد:
public object Invoke(Delegate method, DispatcherPriority priority, params object[] args);
همانطور که در بالا میبینید، این متد یک Delegate را که به متدی اشاره میکند و قرار است در UIThread اجرا شود، دریافت میکند و یک اولویت به آن میدهد. پارامترهای بعدی، آرگومانهایی است که به متد مقصد پاس داده میشوند.
کنترل انجام کار
در هر دو روشی که برای WinApp و WPF App گفته شد، مشکلی وجود دارد: امکان نمایش گزارشی از روند انجام کار بهراحتی وجود ندارد و این کار مستلزم صدها خط کد و استفاده از توابع گوناگون است. بههمین دلیل تیم برنامهنویسی داتنت در نسخه 2 کنترل جدیدی به نام BackgrroundWorker را برای اینکار طراحی کردند، این کنترل برای انجام عملیات زمان بر استفاده میشود و قابلیت نمایش گزارش روند انجام کار را نیز دارد.
در زیر نحوه استفاده از این کنترل را با یک مثال شرح خواهیم داد:
ابتدا یک نمونه از این کنترل ایجاد میکنیم، برای اینکار میتوان از جعبه ابزار (Toolbox) استفاده کرد یا اینکه دستی کدهای مربوطه را نوشت. بعد از ساختن یک نمونه ویژگیها و رخدادهای آن را مقدار دهی میکنیم:
mBW = new BackgroundWorker();
mBW.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(
mBW_RunWorkerCompleted);
mBW.ProgressChanged +=new ProgressChangedEventHandler
(mBW_ProgressChanged);
mBW.DoWork += new DoWorkEventHandler(mBW_DoWork);
mBW.WorkerReportsProgress = true;
mBW.WorkerSupportsCancellation = true;
در قطعه کد بالا ابتدا رخدادهای کنترل BackgroundWorker را مقداردهی کردهایم. رخداد اول RunWorkerCompleted است. این رخداد متد پاس داده شده به آن را زمانی اجرا میکند که عملیات تمام شده باشد، کنسل شده باشد یا خطای در حین اجرا رخ داده باشد.
رخداد بعدی ProgressChanged است، این رخداد زمانی فعال میشود که متد ReportProgress کنترل BackgroundWorker اجرا شود.
رخداد دیگر DoWork است، این رخداد زمانی فعال میشود که متد RunWorkAsync اجرا شود.
دو ویژگی دیگر که با مقدار True مقداردهی شدهاند، بهترتیب نمایانگر این ویژگیاند که گزارش پیشرفت کار بروزرسانی شود یا خیر، و دیگری نمایانگر این است که عملیاتی که توسط این کنترل انجام میشود، قابلیت لغوشدن را دارد یا خیر.
مثال کامل استفاده از BackgroundWorker هم در WPF هم برای Windows Formرا میتوان از لینک زیر دانلود نمایید:
مراجع:
C# 2.0, The Complete Reference, Herbert Schildt, Osborne Pub, 2006
Windows Form 2 Programming, Chris Sells, Addison Wesley, 2003
Professional WPF Programming, Chris Andrade, Wrox Pub, 2007
امیربهاالدین سبطالشیخ