‏نمایش پست‌ها با برچسب Windows forms. نمایش همه پست‌ها
‏نمایش پست‌ها با برچسب Windows forms. نمایش همه پست‌ها

۱۳۸۷/۱۲/۱۴

مروري بر كاربرد DoEvents


چند روز قبل هنگام استفاده از DoEvents در يك برنامه windows forms ، ناگهان پيغام stack overflow ظاهر شد! براي علت يابي و رفع آن كمي جستجو كردم كه خلاصه‌ي آن به شرح زير است:


DoEvents چيست؟

DoEvents يكي از متدهاي كلاس Application در فضاي نام System.Windows.Forms است.
ويندوز جهت مديريت رخدادهاي مختلف از يك صف استفاده مي‌كند. رخدادهايي مانند كليك ماوس، تغيير اندازه‌ي يك فرم و مواردي شبيه به آن ابتدا در يك صف قرار مي‌گيرند و سپس پردازش مي‌شوند. زمانيكه كنترلي مشغول پاسخ دهي به يك رخ‌داد مي‌گردد، ساير رخ‌دادها هنوز در صف هستند و پردازش نخواهند شد. بنابراين اگر برنامه‌ي شما در يك روال رخ‌دادگردان كليك، عملياتي طولاني را در حال انجام باشد، بدليل عدم پردازش ساير رخ‌دادها اينطور به نظر خواهد رسيد كه هنگ كرده است.
روش صحيح پردازش يك عمليات طولاني استفاده از يك ترد ديگر مي‌باشد تا ترد اصلي برنامه كه كار مديريت رابط كاربر برنامه را به عهده دارد، درگير اين عمليات طولاني نشده و پاسخگوي رخ‌دادهاي رسيده باشد.
راه ميان‌بر و ساده‌اي كه اينجا وجود دارد استفاده از DoEvents مي‌باشد (بدون ايجاد يك ترد جديد). براي مثال اگر در روال رخ دادگردان كليك يك برنامه، حلقه‌اي طولاني در حال پردازش است، هر از چندگاهي اين متد فراخواني شود، رخ‌دادهاي در صف قرار گرفته فرصت ارسال به ترد اصلي برنامه را يافته و برنامه در حالت هنگ به نظر نخواهد رسيد.
براي نمونه مثال زير را در دو حالت با Application.DoEvents و بدون آن اجرا كنيد:

private void btnProcessWithDoEvents_Click(object sender, EventArgs e)
{
for (int i = 0; i < 100000; i++)
{
TextBox1.Text = "Processing " + i.ToString();
Application.DoEvents();
}
}

در حالت بدون استفاده از Application.DoEvents ، تنها آخرين عبارت پردازش شده را در TextBox1 مشاهده خواهيد كرد و همچنين در اين حين، برنامه در حالت هنگ به نظر مي‌رسد و برعكس.

مشكلات احتمالي حاصل از استفاده از Application.DoEvents :

الف) حس غلط پايان يافتن عمليات پيش از موعد
در مثال فوق در حين استفاده از Application.DoEvents ، دكمه‌ي btnProcessWithDoEvents مجددا فعال شده و قابل كليك كردن مي‌شود ولي آيا اين بدين معنا است كه پردازش قبلي به پايان رسيده است؟ به يك سري از كاربرها هم click-happy user گفته مي‌شود! يعني از كليك كردن مجدد لذت مي‌برند! در اين حالت حتما بايد دكمه‌ي btnProcessWithDoEvents را در ابتداي پردازش غيرفعال كرد و سپس در انتهاي آن بايد مجددا فعال شود.
مورد مشكل كليك مجدد حتي مي‌تواند منجر به تخريب اطلاعات در حال پردازش شود. فرض كنيد برنامه در حال ذخيره‌ي اطلاعات در يك فايل است و كاربر مرتبا بر روي دكمه‌ي پردازش مربوطه كليك كنيد. فايل نهايي از يك سري اطلاعات ناهماهنگ و بي‌ربط پر خواهد شد.

ب) مشكل stack overflow
اگر علاقمند باشيد، اين مورد را مي‌توان به صورت زير شبيه سازي كرد:

يك تايمر را به برنامه اضافه كنيد و يك دكمه. در روال رخ‌دادگردان كليك مربوط به دكمه، دستورات زير را اضافه كنيد:

private void btnStartTimer_Click(object sender, EventArgs e)
{
this.timer1.Enabled = true;
this.timer1.Start();
this.timer1.Interval = 20;

}
و در روال tick مربوط به تايمر، دستورات زير را اضافه كنيد:

private void timer1_Tick(object sender, EventArgs e)
{
Thread.Sleep(50);
Application.DoEvents();
}
برنامه را اجرا كرده و يكي دو دقيقه صبر كنيد، حتما با پيغام خطاي stack overflow مواجه خواهيد شد. چرا؟
فواصل زماني اجراي تايمر به 20 ميلي ثانيه تنظيم شده است اما در روال رخ‌داد گردان tick آن، نياز به 50 ميلي ثانيه (بيش از 20 ميلي ثانيه) يا بيشتر براي اجرا دارد. با رسيدن به Application.DoEvents ، رخ‌داد در صف قرار گرفته‌ي ديگر tick بلافاصله اجرا مي‌شود و همينطور الي آخر، تا بالاخره stack overflow حاصل خواهد شد.


پس چه بايد كرد؟

الف) هنگام استفاده از Application.DoEvents به موارد فوق حتما دقت داشته باشيد.
ب) بجاي استفاده از اين روش كه در بيشتر موارد يك ضعف برنامه نويسي محسوب مي‌شود، شروع به استفاده از روش‌هاي غيرهمزمان نمائيد. براي مثال استفاده از :
BackgroundWorker
Asynchronous delegates
Threads

تنها موردي را كه هنگام كار با تردها بايد در نظر داشت اين است كه امكان دسترسي به كنترل‌هاي يك فرم را از ترد ديگري كه آن كنترل را ايجاد نكرده است، نداريد و براي اين مورد راه‌ حل‌هاي زيادي موجود است.
همچنين بخاطر داشته باشيد در يك ترد استفاده از Application.DoEvents هيچ معنايي ندارد. ترد اصلي برنامه وظيفه‌ي به روز رساني رابط كاربر برنامه و پاسخگويي به رخ‌دادهاي رسيده را به عهده دارد. زمانيكه پردازش در تردي ديگر صورت مي‌گيرد، ترد اصلي برنامه تا پايان پردازش متد شما قفل نخواهد شد كه نيازي به استفاده از اين متد باشد. در اين حالت استفاده از Application.DoEvents ، سبب بالا رفتن مصرف حافظه‌ي برنامه و همچنين بالا رفتن ميزان مصرف CPU خواهد شد.

جهت مطالعه بيشتر
Keeping your UI Responsive and the Dangers of Application.DoEvents


۱۳۸۷/۱۲/۰۹

مشكل چشمك زدن (Flicker) در كنترل ListView


با هر بار اضافه كردن يك سطر به ListView ، تمام ناحيه پس زمينه كنترل به روز شده و مشكل چشمك زدن (Flicker) آزار دهنده‌اي را پديد مي‌آورد. راه حل‌هاي زيادي براي رفع اين مشكل وجود دارد. براي مثال استفاده از متدهاي BeginUpdate و EndUpdate قبل و پس از افزودن تعداد زيادي ركورد به يك ListView . اما اگر اين كنترل توسط چند ترد در حال به روز رساني باشد و هربار هم تعداد آيتم‌هاي اضافه شده آنچنان زياد نباشد، اين روش اثري نداشته و باز هم مشكل flickering وجود خواهد داشت.
رفع اين مشكل راه حل بسيار ساده‌اي دارد كه به شرح زير است:

يك user control جديد ايجاد كنيد، آن‌را از ListView به ارث برده و سپس سطر زير را به constructor آن اضافه كنيد:

this.DoubleBuffered = true;

اكنون از اين ListView سفارشي بجايlistView استاندارد استفاده كنيد، مشكل برطرف مي‌شود!

public partial class CustomListView : ListView
{
public CustomListView()
{
this.DoubleBuffered = true;
}
}
شبيه به همين مورد را جهت كنترل ListBox نيز مي‌توان پياده سازي كرد

۱۳۸۷/۱۰/۱۴

به دام انداختن خطاهاي مديريت نشده در برنامه‌هاي Windows forms دات نت


شبيه به نحوه‌ي به دام انداختن خطاهاي مديريت نشده در Web forms و روال استاندارد Application_Error ، در برنامه‌هاي Windows forms نيز اين امر به صورت زير ممكن است:


using System;
using System.Threading;
using System.Windows.Forms;

namespace testWinForms87
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
// handling UI thread exceptions
Application.ThreadException += uIThreadException;

// force all Windows Forms errors to go through our handler.
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

// handling non-UI thread exceptions.
AppDomain.CurrentDomain.UnhandledException += currentDomainUnhandledException;


Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}

private static void currentDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
MessageBox.Show(((Exception)e.ExceptionObject).Message, "currentDomainUnhandledException");
}

private static void uIThreadException(object sender, ThreadExceptionEventArgs e)
{
MessageBox.Show(e.Exception.Message, "uIThreadException");
}
}
}

چند نكته:
الف)همانطور كه ملاحظه مي‌كنيد سطرهاي فوق بايد قبل از Application.Run در روال اصلي برنامه تعريف شوند.
ب) اين متدها استاتيك هستند و توصيه شده است در پايان برنامه ارجاعات آنها را حذف كنيد تا نشتي حافظه رخ ندهد. دقيقا به همين صورت =+ كه اضافه شدند با =- هم قابل حذف هستند.
ج) در حالت اجرا شدن uIThreadException ، برنامه بسته نخواهد شد (و بديهي است در صورت عدم بكار گيري اين روش، حتما برنامه كرش خواهد كرد). براي مثال شايد علاقمند نباشيد كه بخاطر عدم دسترسي نوشتن در پوشه‌اي خاص، خطاي حاصل سبب بسته شدن كل برنامه شود. به اين صورت اين موارد را مي‌توان به دام انداخت. اما currentDomainUnhandledException كه حاصل از خطاهاي ايجاد شده براي مثال در يك ترد ديگر بجز ترد اصلي برنامه هستند، حتما سبب بسته شدن برنامه خواهند شد. بنابراين اينجا تنها شانس لاگ كردن خطاي مديريت نشده حاصل را خواهيم داشت. به همين منظور هميشه توصيه مي‌شود كه در تردهاي ايجاد شده در برنامه، حتما موارد مديريت خطاها را لحاظ نمائيد، زيرا خطاهاي حاصل شده در آن‌ها غيرقابل اغماض بوده و حتما سبب كرش برنامه مي‌شوند.

پ.ن.
دقيقا در برنامه‌هاي Win32 دلفي هم چنين قابليتي به همين شكل و تقريبا با همين نام‌ها وجود دارد. فقط كافي است روالي را جهت Application.OnException ايجاد كنيد: ;)

procedure TmyFrmMain.FormCreate(Sender: TObject);
begin
Application.OnException := MyExceptionHandler;
end;
procedure TmyFrmMain.MyExceptionHandler(Sender: TObject; E: Exception);
begin
ShowMessage(e.Message);
end;