User control constructor called twice

R

Rimpinths

I'm new at developing user controls in C#, and one thing I've noticed
right off the bat is that the constructor gets called twice -- once at
design time, once at run time.

In short, I'm trying to develop a control derived from DataGridView. It
will have a default set of columns (but I don't want to create them via
the Properties window for various reasons) so I added a call to a
method AddColumns() in the constructor. But the columns end up getting
added twice -- once when I place the control in a form at design time,
and then again when I run the application.

I know that I can get around this by making an explicit call to
AddColumns() at runtime rather than putting it in the constructor. But
I like the fact that they show up at design time so that way I can see
what it's going to look like.

I tried adding an "initialized" flag in my constructor to prevent it
from callling AddColumns() twice, but that doesn't work because
apparently its design time value is reset at run time.

Any suggestions?
 
R

Rimpinths

Following up on my own message...

I thought that I could at least work around this problem by calling
Columns.Clear() in my AddColumns() method, but even though the column
count is zero after I clear them, two sets of the columns still show
up. So where is the other set being stored?

I think I'm missing some fundamental understanding about user controls.
Is the moral of the story to just not do anything in the constructor?
 
D

Dave Sexton

Hi,

The designer is creating an instance of your control at design-time. If it
didn't, you wouldn't see anything :)

When the designer creates your control, the constructor is executed and the
columns are added. Since the columns can be "designed", the designer
recognizes this and serializes code (I'm assuming) into your code-behind to
initialize the columns. I suspect that you see the columns in the grid even
at design-time, correct?

Of course, when you run the application the constructor executes and adds
the columns at runtime. This probably occurs in the InitializeComponent
method of your Form, also in which the code was serialized to initialize the
columns at design-time.

So after your control is constructed in the InitializeComponent method,
columns are added again by the designer-generated code.

You can try to prevent the addition of the columns when the control is being
hosted in a designer. In order to do this you need to check the
Site.DesignMode property of the Control, however Site will always be null in
the constructor. So you'll have to move the column-addition code into
another method that can evaluate Site.DesignMode:

protected override void OnControlAdded(ControlEventArgs e)
{
if (Site == null || !Site.DesignMode)
{
// add columns
}

base.OnControlAdded(e);
}

Realize that the columns shouldn't appear when the control is added to a
Form's designer.

Let us know if that works for you :)
 
R

Rimpinths

Thanks for your detailed response, Dave.

Yeah, I guess it does make sense that the constructor gets called at
design time, and then of course at run time. What I don't get is why
the constructor gets called twice for the same instance of that
control? Why doesn't the design time instance get thrown out and a new
one created at run time? How can you even call a constructor twice for
the same instance?

Unfortunately, your suggestion doesn't work. Actually, it's worse -- it
ends up adding two columns at design time and another two columns at
run time. Go figure. And after adding it and removing it from my form
several times, the form designer code has references to a
dataGridViewTextBoxColumn5 and dataGridViewTextBoxColumn6, and so on.
Weird stuff. I'm still trying to get my head around the original
problem, not sure what's going on with your suggestion.

Here's another interesting test that I ran. I tried this bit of code in
the constructor...

Columns.Add("column1", "column1");
Rows.Add(3);
Rows[0].Cells[0].Value = "Hello,";
Rows[1].Cells["Column1"].Value = "world!";

try
{
Rows[2].Cells[1].Value = "I'm here.";
}
catch (ArgumentOutOfRangeException)
{
Rows[2].Cells[0].Value = "I can't find you";
}

Everything appears in the first column at design time and run time. And
the ArgumentOutOfRangeException is raised even at run time, despite the
fact that I can see that second column. How can you even access it?
Where does it exist?

I know that we developers are quick to attribute something to a bug
when it doesn't work the way that we expect it to, but this seems like
a genuine bug to me. I'm not sure if this is the general behavior of
constructors of user controls, or more likely, a particular problem
with the DataGridView control which I think went through a lot of
changes in .NET 2.0. I'm trying the think of similar scenario with
another control, but I can't. Oh wait, let me try deriving a ComboBox
and call this in the constructor...

Items.Add("Item #1");
Items.Add("Item #2");

Yep, same problem. Four items are listed, but Items.Count returns 2.
What the...? This seems like such a basic problem if I ran into this
after two days of playing around with derived controls. There must be
some way around it, perhaps something that I may not be doing right
after all, but I still think the proper behavior would be to throw out
the design time instance and start over at run time.

I'll probably just go back to calling a separate AddColumns method at
runtime as I said in my original message. It's just not worth the
effort to try figure out what's going on here, although I'm still
curious, just for the sake of my C# knowledge.

Thanks again for your help, much appreciated.
 
R

Rimpinths

Thanks for your detailed response, Dave.

Yeah, I guess it does make sense that the constructor gets called at
design time, and then of course at run time.

One other thing that I noticed is that Visual Studio adds
this.dataGridViewDerived1.Columns.AddRange to the design code of the
form that the control gets added to. Anything you do in the constructor
of the user control gets added to the design code of the control's
container. I'm not sure if I understand the logic behind that behavior.
And one solution I did find is to remove the Columns.AddRange in the
design code after I add the control to a form, but what a hassle, why
should I need to do that?

Unfortunately, your suggestion doesn't work. Actually, it's worse -- it
ends up adding two columns at design time and then another two columns
at run time. Go figure. And after adding it and removing it from my
form several times, the form designer code has references to a
dataGridViewTextBoxColumn5 and dataGridViewTextBoxColumn6, and so on.
Weird stuff. I'm still trying to get my head around the original
problem, not sure what's going on with your suggestion.

Here's another interesting test that I ran. I tried this bit of code in
the constructor...

Columns.Add("column1", "column1");
Rows.Add(3);
Rows[0].Cells[0].Value = "Hello,";
Rows[1].Cells["Column1"].Value = "world!";

try
{
Rows[2].Cells[1].Value = "I'm here.";
}

catch (ArgumentOutOfRangeException)
{
Rows[2].Cells[0].Value = "I can't find you";
}

How can I even access that other column that I can see? Who does it
even belong to? I'm trying the think of similar scenario with another
control, but I can't. Oh wait, let me try deriving a ComboBox and call
this in the constructor...

Items.Add("Item #1");
Items.Add("Item #2");

Yep, same problem. Four items are listed, but Items.Count returns 2.
What the...? This seems like such a basic problem if I ran into this
after two days of playing around with derived controls. There must be
some way around it, perhaps something that I may not be doing right
after all, but I still think the proper behavior would be to throw out
the design time instance and start over at run time.

I'll probably just go back to calling a separate AddColumns method at
runtime as I said in my original message. It's just not worth the
effort to try figure out what's going on here, although I'm still
curious, just for the sake of my C# knowledge.

Thanks again for your help, much appreciated.

Mike
 
D

Dave Sexton

Hi,

I'm so sorry - I accidentally gave you code overloading the wrong method :|

The following code works just fine:

public class CustomDataGridView : DataGridView
{
protected override void OnCreateControl()
{
if (Site == null || !Site.DesignMode)
{
Columns.Add("Test 1", "Header 1");
Columns.Add("Test 2", "Header 2");
}

base.OnCreateControl();
}
}
Yeah, I guess it does make sense that the constructor gets called at
design time, and then of course at run time. What I don't get is why
the constructor gets called twice for the same instance of that
control? Why doesn't the design time instance get thrown out and a new
one created at run time? How can you even call a constructor twice for
the same instance?

You can't call a constructor twice on the same instance. The runtime
instance and design-time instance are not the same. When you start your
application, even with a debugger attached, it will run in its own process.

<snip - sorry - >
 
D

Dave Sexton

Hi,
One other thing that I noticed is that Visual Studio adds
this.dataGridViewDerived1.Columns.AddRange to the design code of the
form that the control gets added to. Anything you do in the constructor
of the user control gets added to the design code of the control's
container. I'm not sure if I understand the logic behind that behavior.

Not just any code is generated.

You've got to realize that the columns may be designed themselves. In other
words, you can add and remove columns from within an editor dialog, which
means that the designer has to serialize code to persist the changes that
you make. Guess where that code is persisted?

The point of a designer is to design the Form for use at runtime, so if you
add a DataGridView with some predefined columns, the designer is going to
see those columns and serialize code to generate them at runtime. It
doesn't know that you've added them yourself in the constructor and not
after the control was added to the Form.
And one solution I did find is to remove the Columns.AddRange in the
design code after I add the control to a form, but what a hassle, why
should I need to do that?

The designer code is "live" code that will be modified by VS on-the-fly.
Changes like that will probably not last for very long. Try closing the
Form designer and reopening it or try closing VS and reopening it and you'll
probably see what I mean.

<snip - still sorry you wasted your time writing this - :) >
 
B

Bruce Wood

Rimpinths said:
One other thing that I noticed is that Visual Studio adds
this.dataGridViewDerived1.Columns.AddRange to the design code of the
form that the control gets added to. Anything you do in the constructor
of the user control gets added to the design code of the control's
container. I'm not sure if I understand the logic behind that behavior.
And one solution I did find is to remove the Columns.AddRange in the
design code after I add the control to a form, but what a hassle, why
should I need to do that?

What is happening here is that the Columns property of the original
data grid contains code that tells the Designer what the default
columns look like. In fact, every property of a control has some code
that tells the Designer what the default is for that property.

When it comes time to serialize the control into code, the Designer
serializes code for all properties that have values _different from
their default values_. It doesn't know that the property's value was
changed in the constructor rather than by the programmer in the
Designer itself. All it knows is that property X has a value other than
default, so it serializes that value.

You need to read up on "design time" logic in Visual Studio and how to
design your own controls. There are several MSDN articles out there,
lots of documentation, and several newsgroups. Some things are very
easy to do, while others are difficult. I still haven't gotten the hang
of compound properties like Columns, but others have, so it can be done
properly.
Unfortunately, your suggestion doesn't work. Actually, it's worse -- it
ends up adding two columns at design time and then another two columns
at run time.

No... it doesn't add "two columns at design time and then another two
columns at run time." What it does is add two columns in the
constructor, and then add them again in the serialized code for the
control. Again, the Designer didn't know that those two columns were
added in the constructor, because you don't have any attributes /
methods to indicate that those two columns are defaults and shouldn't
be serialized.

In fact, this points out why having some "default columns" may be a
problem: if you then add a third column at design time the Designer
will see that the value of Columns is not the default of two columns
and will then serialize code to add all three columns, resulting in
five. You may have to write your own serialization code in order to get
this to work, and IMHO that's pretty advanced design-time stuff.
Probably not worth the hassle, although I'm told that it can be done.
Go figure. And after adding it and removing it from my
form several times, the form designer code has references to a
dataGridViewTextBoxColumn5 and dataGridViewTextBoxColumn6, and so on.

The names for the columns seem to be monotonically incrementing from
the highest name used so far. The Designer doesn't appear to try
reusing column names that are free. Not sure why, but there you go.
Here's another interesting test that I ran. I tried this bit of code in
the constructor...

Columns.Add("column1", "column1");
Rows.Add(3);
Rows[0].Cells[0].Value = "Hello,";
Rows[1].Cells["Column1"].Value = "world!";

try
{
Rows[2].Cells[1].Value = "I'm here.";
}

catch (ArgumentOutOfRangeException)
{
Rows[2].Cells[0].Value = "I can't find you";
}

How can I even access that other column that I can see? Who does it
even belong to?

Since you don't say what the result of running the code is, I'm not
sure what the issue is here....
I'm trying the think of similar scenario with another
control, but I can't. Oh wait, let me try deriving a ComboBox and call
this in the constructor...

Items.Add("Item #1");
Items.Add("Item #2");

Yep, same problem. Four items are listed, but Items.Count returns 2.

What do you mean, "Four items are listed"? Listed how? How do you know
that there are supposedly four? Where are you checking Items.Count?
 
R

Rimpinths

The following code works just fine:

Yes, it does, thank you!
You can't call a constructor twice on the same instance. The runtime
instance and design-time instance are not the same. When you start your
application, even with a debugger attached, it will run in its own process.

Yeah, I realized that didn't make any sense about two minutes after I
posted that message and consequently deleted it. I considered that I
never run it as an exe, and then I saw that it did the same thing when
I did, which blows the design time and runtime instance out of
competition.

Mike
 
R

Rimpinths

The point of a designer is to design the Form for use at runtime, so if you
add a DataGridView with some predefined columns, the designer is going to
see those columns and serialize code to generate them at runtime. It
doesn't know that you've added them yourself in the constructor and not
after the control was added to the Form.

Okay, I think that nails the concept that I was missing: serialization.
This is what I need to learn more about.
<snip - still sorry you wasted your time writing this - :) >

No, not a waste of time at all. I'm so grateful that people like you
are willing to spend time answering questions from strangers.

I could've worked around this problem in no time, but I knew that I
should probably try to figure out what was going on, it could help my
C# knowledge, and it has.

Mike
 
R

Rimpinths

When it comes time to serialize the control into code, the Designer
serializes code for all properties that have values _different from
their default values_. It doesn't know that the property's value was
changed in the constructor rather than by the programmer in the
Designer itself. All it knows is that property X has a value other than
default, so it serializes that value.

Here we go again, serialization, this is what I need to learn more
about.
You need to read up on "design time" logic in Visual Studio and how to
design your own controls. There are several MSDN articles out there,
lots of documentation, and several newsgroups. Some things are very
easy to do, while others are difficult.

Yeah, I'm figuring that out. I was actually just wanted to create a few
controls that I could reuse within a project, to standardized fonts and
colors and such. If I wanted to sell it or use it throughout several
projects, I'd problem spend more time understanding the problem.

I think that this is probaby an aytpical case. The core problem is
adding objects to a collection (columns to a grid, items to a combobox)
in the constructor. In most cases, performing tasks in the constructor,
like setting properties and such, this wouldn't be a problem.
Since you don't say what the result of running the code is, I'm not
sure what the issue is here....

What, you can't do it in your head?

Sorry, the resulting code is that two columns are displayed with the
headers "column1", and everything gets written to the first column. If
I try to access the second columns with Rows[2].Cells[1], it throws
ArgumentOutOfRangeException. If I ask how many columns are there, it
returns Columns.Count = 1. You can't access that second column in any
way that I can figure out, so where does that object exist?
What do you mean, "Four items are listed"? Listed how? How do you know
that there are supposedly four? Where are you checking Items.Count?

I'm checking Items.Count at runtime, after the constructor has been
called. The ComboBox displays 4 choices in the drop down menu, but
Items.Count says that there are only 2 items. It's a similar problem as
the one with the columns above.

Mike
 
D

Dave Sexton

Hi Mike,

I could've worked around this problem in no time, but I knew that I
should probably try to figure out what was going on, it could help my
C# knowledge, and it has.

I admire your desire to learn instead of just copying :)
 

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