Forms, Parent property and Threads, where are the rules? (Very long but very intersting)

M

minimega

Hello to all NG.

I'm developing a .NET application with CF 1.0 on a x86 WinCE 4.2
device. The application must do some long elaborations, so I want to
display a foreground form, with an animation (a clock, a running car, a
moving butterfly..) that runs until the elaborations finished, so user
can have the idea that when somthing is moving, the PC is working...

I've just implemented the "WaitingForm" with a timer that displays
images sequence that do the animation (20 bitmaps images). That's works
fine. However, if I create the instance and then .Show() the instance
in the main thread, when I start the elaboration the animation become
"in jerks" and stops when I make some syncronous PInvoke calls to
external dll or when I access data etc..

So I've moved the instancing of the WaitingForm in a second thread, so
the WaitingForm has its own MessageQueue and is independent from the
main thread. Thats works fine and the animation is very fluid and
without locks (except when I move the mousepointer over the bitmpa
area.. WHY?!?!).

However, when I show the WaitingForm, it is displayed as a new window,
also in the taskbar, so, if the user switchs to other windows or to the
main application window (wich is working), the WaitingForm disappers
under the new activated window, and the animation is no more visible.
So, I can try to use the OnTop property (does it exists in WinCE?) but
what I'm trying to do is to "incorporate" the WaitingForm in the main
application window, like if it were a standard form control, placed on
the surface of the main application window (a sort of MDI-MDIChild
application). I now I can do it setting the WaitingForm .Parent
property to the main application window reference (this).

However this way raieses an exception, and I'm investigating about
this. Please, start a new VS2003 Smart Device C# project, add Form1 and
Form2 forms and cut&past the following code:

************ Form1.cs ***************

using System;
using System.Drawing;
using System.Collections;
using System.Windows.Forms;
using System.Data;
using System.Threading;

namespace FormsAndThreads
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.Button button4;
private System.Windows.Forms.Button button5;
private System.Windows.Forms.Button button3;

private Form2 frmThread = null;

public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.button3 = new System.Windows.Forms.Button();
this.button4 = new System.Windows.Forms.Button();
this.button5 = new System.Windows.Forms.Button();
//
// button1
//
this.button1.Location = new System.Drawing.Point(20, 16);
this.button1.Size = new System.Drawing.Size(112, 60);
this.button1.Text = "Normal";
this.button1.Click += new
System.EventHandler(this.button1_Click);
//
// button2
//
this.button2.Location = new System.Drawing.Point(20, 88);
this.button2.Size = new System.Drawing.Size(112, 60);
this.button2.Text = "Parent";
this.button2.Click += new
System.EventHandler(this.button2_Click);
//
// button3
//
this.button3.Location = new System.Drawing.Point(20, 160);
this.button3.Size = new System.Drawing.Size(112, 60);
this.button3.Text = "Thread";
this.button3.Click += new
System.EventHandler(this.button3_Click);
//
// button4
//
this.button4.Location = new System.Drawing.Point(20, 232);
this.button4.Size = new System.Drawing.Size(112, 60);
this.button4.Text = "Parent Thread";
this.button4.Click += new
System.EventHandler(this.button4_Click);
//
// button5
//
this.button5.Location = new System.Drawing.Point(20, 308);
this.button5.Size = new System.Drawing.Size(112, 60);
this.button5.Text = "Parent Thread 2";
this.button5.Click += new
System.EventHandler(this.button5_Click);
//
// Form1
//
this.Controls.Add(this.button5);
this.Controls.Add(this.button4);
this.Controls.Add(this.button3);
this.Controls.Add(this.button2);
this.Controls.Add(this.button1);
this.Text = "Form1";
this.Load += new System.EventHandler(this.Form1_Load);

}
#endregion

/// <summary>
/// The main entry point for the application.
/// </summary>

static void Main()
{
Application.Run(new Form1());
}

private void Form1_Load(object sender, System.EventArgs e)
{
}

private void button1_Click(object sender, System.EventArgs e)
{
// Create a new Form2 instance
Form2 frmForm2 = new Form2();
frmForm2.Show();

// Wait 2 seconds on the main thread
Thread.Sleep(2000);
}

private void button2_Click(object sender, System.EventArgs e)
{
// Create a new Form2 instance and "incorporate" it in Form1
(looks like MDI-MDI child applications)
Form2 frmForm2 = new Form2();
frmForm2.Parent = this;
frmForm2.Show();

// Wait 2 seconds on the main thread
Thread.Sleep(2000);
}

private void button3_Click(object sender, System.EventArgs e)
{
// Starts a new thread
Thread thrThread = new Thread(new ThreadStart(Thread_CallBack));
thrThread.Start();

// Wait 2 seconds on the main thread
Thread.Sleep(2000);
}

private void Thread_CallBack()
{
Form2 frmForm2 = new Form2();

// frmForm2.Show(); // Can't be used the .Show method because it
doesn't suspend the execution of the thread
// and after the thread-exit the istance is
automatically set to null and the form disappear

Application.Run(frmForm2);
}

private void button4_Click(object sender, System.EventArgs e)
{
// Starts a new thread
Thread thrThread = new Thread(new
ThreadStart(ParentThread_CallBack));
thrThread.Start();

// Wait 2 seconds on the main thread
Thread.Sleep(2000);
}

private void ParentThread_CallBack()
{
// Create a new Form2 instance
Form2 frmForm2 = new Form2();

// Set the .Parent property (this is doing by the second thread)
frmForm2.Parent = this; // Crash!!!!

Application.Run(frmForm2);
}

private void button5_Click(object sender, System.EventArgs e)
{
// Exit if already instanced
if (frmThread != null) return;

// Starts a new thread
Thread thrThread = new Thread(new
ThreadStart(ParentThread2_CallBack));
thrThread.Start();

// Wait until new form instance is created by the started thread,
then continue
while (frmThread == null)
Thread.Sleep(50);

// Set the .Parent property (this is doing by the main thread)
frmThread.Parent = this;

// Wait 2 seconds on the main thread
Thread.Sleep(2000);

}

private void ParentThread2_CallBack()
{
// Create a new Form2 instance.
frmThread = new Form2();

// Wait until the main thread sets the .Parent property, then
continue
while (frmThread.Parent == null)
Thread.Sleep(50);

Application.Run(frmThread);
}


}
}

******* Form 2 *******

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;

namespace FormsAndThreads
{
/// <summary>
/// Summary description for Form2.
/// </summary>
public class Form2 : System.Windows.Forms.Form
{
private System.Windows.Forms.Timer timer1;
private System.Windows.Forms.Label label1;

private int counter = 0;

public Form2()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();

//
// TODO: Add any constructor code after InitializeComponent call
//
}

/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}

#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.timer1 = new System.Windows.Forms.Timer();
this.label1 = new System.Windows.Forms.Label();
//
// timer1
//
this.timer1.Enabled = true;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
//
// label1
//
this.label1.Font = new System.Drawing.Font("Microsoft Sans
Serif", 48F, System.Drawing.FontStyle.Regular);
this.label1.Location = new System.Drawing.Point(40, 40);
this.label1.Size = new System.Drawing.Size(96, 72);
this.label1.Text = "0";
this.label1.TextAlign =
System.Drawing.ContentAlignment.TopCenter;
//
// Form2
//
this.BackColor =
System.Drawing.Color.FromArgb(((System.Byte)(224)),
((System.Byte)(224)), ((System.Byte)(224)));
this.ClientSize = new System.Drawing.Size(178, 164);
this.Controls.Add(this.label1);
this.Text = "Form2";
this.Load += new System.EventHandler(this.Form2_Load);
this.Closed += new System.EventHandler(this.Form2_Closed);

}
#endregion

private void timer1_Tick(object sender, System.EventArgs e)
{
counter = counter + 1;
if (counter > 10) counter = 0;

label1.Text = counter.ToString();
}

private void Form2_Load(object sender, System.EventArgs e)
{
// Force the screen visualization
this.Visible = true;
this.Refresh();
this.BringToFront();
this.Focus();
}

private void Form2_Closed(object sender, System.EventArgs e)
{
timer1.Enabled = false;
}
}
}



Comments:

Form2 is a small form that has a timer that shows sequence numbers fro
0 to 9 every 100 ms. When you close it via the close button the timer
is disabled to avoid null references after the form is set to null.

Form1 is the main application window and has 5 buttons. Every button
shows a new instance of Form2 (except for button 5) in a various
manner. After shows the window, every button makkes the main thread
sleep for 2 seconds (emulating a strong data elaboration for the main
thread).

Look at the code:

button1_Click: create a new (local) instance of Form2 and shows it as
self window (visible also in the taskbar; TaskManager shows Form1 and
Form2 in "active applications" list). New Form2 instance is locked for
2 seconds, until main thread Sleep() function return. That's why new
Form2 instance is instanced in the main thread, that's sleeping.

button2_Click: create a new (local) instance of Form2 and before shows
it, it set his .Parent proprerty to "this". The new Form2 is
incorporated (owned) in the main application window, as if it were a
MDIChild. The new Form2 is no more visible in the taskbar and
TaskManager shows only Form1 in "active applications" list. New Form2
instance is also locked for 2 seconds, until main thread Sleep()
function return. That's why new Form2 instance is instanced in the main
thread, that's sleeping.

button3_Click: starts a new thread and sleep immediatelly for 2
seconds. This sleep let the new thread start, creating a new (local)
instance of Form2 and showing it as self window (visible also in the
taskbar; TaskManager shows Form1 and Form2 in "active applications"
list). New Form2 instance starts immediatelly to show the numers
sequence, even if the main thread is still sleeping. In fact, the new
Form2 window has the focus (his title bar is blue), but also the main
application window is blue, but only for 2 more seconds (remember it's
sleeping). After that time, the main application window can process his
WindowMessage queue and disable the title bar. This is the
demonstration that the two thread are (obviously) independent each
other.

This is the right way: a WaitingForm that can do his work in a
independent way from the main (hard-working) thread. Now I want to
"incorporate" the Form2 in Form1like button2_Click does: HERE STARTS
THE PROBLEM.

Watch at button4_Click code: it's the "sum" of button3_Click and
button2_Click code. Setting the .Parent property in the second thread
code, to a window reference that runs in the main thread, raises a
System.ArgumentException error.

IS ANYBODY ABLE TO EXPLAIN ME WHY THIS HAPPENS? What are the limits of
using controls, properties, "shared" variabile across multiple threads?

I know (after watch at some threads examples - first of all
SplashScreen from Microsoft) I've understand tha the calls across
multimple thread must been "safe", and must be doing with the Invoke
method.

So, now, watch at button5_Click code: frmThread is declared at class
level, not at function level, so the object is visible to button5_Click
but also to ParentThread2_CallBack functions.. However, in this way we
can have only one instance of Form2, but this is another problem.

What I do is (on button5_Click) to start a new thread and imediatelly
sleep until the frmThread object is instanced from the started thread.
The sleep is made into a while cycle, with a interval of 50 ms, so the
lock-mechanism is quite quick. The sleep in the main thread let the
second thread start, so can instance the frmThread object; after doing
that, it wait for the .Parent property to be set by the main thread.
The main thread return from his Sleep and see that frmThread object was
set, so continue with the executing of his code, setting the .Parent
property to "this", then sleeps for 2 more seconds. This new sleep let
the started thread to see that .Parent property was set by main thread
and shows the Form2 (common) instance. Thah works!

IS ANYBODY ABLE TO EXPLAIN ME WHY THIS WAY WORKS?

In detail, when the new Form2 instance is displayed in the main
application window, the counter don't start immediatelly, but wait 2
seconds, until the main thread exits from the sleep, but it would not
have to happen in a thread-programming. Can be that "incorporating" the
Form2 in Form1 causes the Form2 message queue be "attached" to Form1
message queue?

However, once the counter starts no more locks happens. In fact, after
the counter starts, try to click Button 2: you can see that main thread
(and his new Form2 instance) stops for 2 seconds, but old Button5
generated window (in a second thread) runs without stops.

NOW, WHAT IS THE RIGHT WAY TO USE WINDOWS AND CONTROLS ACROSS MULTIPLE
THREAD. HOW CAN BE METHOS, PROPERTY AND OBJECT REFERENCE PASSED IN A
SAFE WAY?

Thanks to all that can give me some explanations,
and thanks to all other people that are arrived to the end of this
(very long) post.

Massimo
 
M

minimega

More detail:

Using the same code in a VS2003 Windows Win32 application, the
button5_Click raises a System.ArgumentException in
system.windows.forms.dll when triyng to execute the "frmThread.Parent =
this;" code line.

Converting and using the same code with VS2005 in a CF 2.0 SmartDevice
application, the button5_Click raises a System.NotSupportedException
when triyng to execute the "frmThread.Parent = this;"; in the immediate
window I get the result:

A first chance exception of type 'System.IO.FileNotFoundException'
occurred in mscorlib.dll
A first chance exception of type 'System.IO.FileNotFoundException'
occurred in mscorlib.dll
A first chance exception of type 'System.NotSupportedException'
occurred in System.Drawing.dll

and in the exception window I get:
Control.Invoke must be used to interact with controls created on a
separate thread.

Good, I must use the .Invoke method, ok, but how can I use .Invoke to
set object properties? This seems to not be supported by CF.

Thanks.
 
C

Chris Tacke, eMVP

Rule #1: All UI elements should be on the main application thread
Rule #2: Only code running in that primary thread context may alter or query
any method, property of a UI element

Your app is violating these rules, and expectedly behaving badly. So how do
you get around it?

My suggestions:

1. Move your animation into a Panel on the Form, so it can't end up
"beneath" another form
2. Move your "elaborations" into a worker thread to run asynchronously
3. If the worker must update the UI, use Control.Invoke
4. If your elaborations continue to affect the system due to high processor
usage, lower the worker thread priority
5. Any Synchronous P/Invokes should be moved to a worker thread for
asynchronous usage if called from the UI thread

--
Chris Tacke
Co-founder
OpenNETCF.org
Are you using the SDF? Let's do a case study.
Email us at d c s @ o p e n n e t c f . c o m
http://www.opennetcf.org/donate
 
F

Fabien Decret

Hi,



I don't know the rules but I have done an application who was quite similar.



I have used a panel and add my form to the panel dynamically :

myForm = new FormXX();

this.myPanel.Controls.Add(myForm);



For the communication between tasks, I use messageQueue, or pass arguments
by reference in the constructor of the form.



In the main Form, I construct all my forms, and hide them and show them when
I need it.



Fabien Decret
 
M

minimega

Thanks Chris,
your suggestions are right, however to implement them I find some
difficulties, why:

2. Move your "elaborations" into a worker thread to run asynchronously.
This is a good choice, so the elaborations are separated by the UI main
thread, however I need to use some variables and objects (database
access, files access, environment variables...) that are owned by the
main UI thread. Is safe to access them by the worker thread without any
safe method (as .Invoke)?

3. If the worker must update the UI, use Control.Invoke
I'm just doing this! However, suppose I have to change the text of a
button from "Stop.." to "Close" on the UI main thread from a worker
thread (because the elaborations are finished). I use the Invoke method
to safe run a "public void ChangeButtonText(object sender,
System.EventArgs e)" on the main UI thread, but how can I pass the new
text of the button? In CF isn't supported to pass parameters like in
full Framework. Can I set a public property in the UI main thread that
_only_ store the value in a his local variable, then call the .Invoke
method that take the value stored before and displays it in the button?

4. If your elaborations continue to affect the system due to high
processor usage, lower the worker thread priority.
This is a good suggestion!

The logic to move all the elaborations on some working threads is a
good solution to solve the problem of the UI main thread, however make
more difficult the development, why I need to keep syncronized main
thread with the working threads, creating a safe way to pass object
reference and variables. I don't know if this is a convenient way, if
the only reson of this is let a simple form with an animation working
on the UI main thread. For now, I've set te WaitingScreen on top,
without change it's parent, so he can run in his working thread and is
always visible also if user switch to other applications. This isn't a
"elegant" solution, but is the only one that let me have a quite good
result and let me develop in a more easy and faster way.

Thanks for your suggestions!

massimo
 
M

minimega

Hi Fabien,
this is the same way I use, except that you put the "child" form into a
panel in the main form, I put it directly in the main form. However
there aren't differences: I can't incorporate a form started by a
working thread (with his own message queue) in a form on the UI main
thread, why between the two messages queue will fall in a dead-lock.
If I want to "incorporate" a form into another, all the two forms must
be owned (istanced) by the UI main thread.

Thanks,
massimo
 
G

Guest

2. Move your "elaborations" into a worker thread to run asynchronously.
This is a good choice, so the elaborations are separated by the UI main
thread, however I need to use some variables and objects (database
access, files access, environment variables...) that are owned by the
main UI thread. Is safe to access them by the worker thread without any
safe method (as .Invoke)?

Sure, just make sure you're thread safe using a synchronization object like
a lock.
3. If the worker must update the UI, use Control.Invoke
I'm just doing this! However, suppose I have to change the text of a
button from "Stop.." to "Close" on the UI main thread from a worker
thread (because the elaborations are finished). I use the Invoke method
to safe run a "public void ChangeButtonText(object sender,
System.EventArgs e)" on the main UI thread, but how can I pass the new
text of the button? In CF isn't supported to pass parameters like in
full Framework. Can I set a public property in the UI main thread that
_only_ store the value in a his local variable, then call the .Invoke
method that take the value stored before and displays it in the button?

Typically you set a private variable, then call Invoke. The invokled
delegate then reads that variable and uses it for the data.
4. If your elaborations continue to affect the system due to high
processor usage, lower the worker thread priority.
This is a good suggestion!

The logic to move all the elaborations on some working threads is a
good solution to solve the problem of the UI main thread, however make
more difficult the development, why I need to keep syncronized main
thread with the working threads, creating a safe way to pass object
reference and variables. I don't know if this is a convenient way, if
the only reson of this is let a simple form with an animation working
on the UI main thread. For now, I've set te WaitingScreen on top,
without change it's parent, so he can run in his working thread and is
always visible also if user switch to other applications. This isn't a
"elegant" solution, but is the only one that let me have a quite good
result and let me develop in a more easy and faster way.

Multithreading is the way you need to go. It's not just the animation that
dies, but your app is likely unresponsive to user input as well, so they'll
think the app is locked up and probably reset the device.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top