How do I update a MainForm label from within a UserControl?

  • Thread starter Thread starter Ken Arway
  • Start date Start date
K

Ken Arway

Using C#, I have a Windows form app (MainForm) which contains a label (MainFormLabel) and a composite User Control (UC1). UC1 consists of a groupbox containing two buttons: UCButton1 and UCButton2.

What I want is to click UCButton1 and have MainFormLabel's text show "Button1", and MainFormLabel's text to show "Button2" when UCButton2 is clicked.

I can't get this to work -- I'm unable to reference MainFormLabel from the UC1.cs code. At best I get an "Object reference not set to an instance of an object" exception, despite using every conceivable variation of MainForm.ActiveForm... in the UC1.cs code.
 
Since your user control is (theoretically, at least) a reusable
component, it shouldn't contain a direct reference to MainForm.

The correct way to do this is using events. Your user control should
expose either one or two events depending upon what the button presses
mean. I'll describe the one-event case first.

1. If the two buttons represent two different answers to the same
question, then your user control should expose a property and an event.
The event should be a simple event that just takes System.EventArgs,
and just indicates that "the user responded". You could call the event:

public System.EventHandler UserReponded;

You would then have a property to indicate how the user responded:

public bool UserReponse
{
get { return this._response; }
}

Each button in your user control would then set the _response flag and
raise a UserResponded event.

Your MainForm would then subscribe to the UserResponded event. The
event handler in the MainForm would then read the UserResponse from the
relevant user control and set the text box contents appropriately.

However, there's another possibility, and that's a scenario in which
the two buttons represent completely different actions that the user
wants to take, not really two answers to the same question. A nicer
design in that case is to have your UserControl expose two events:

public System.EventHandler UserCanceled;
public System.EventHandler UserChangedBackgroundColor;

or some such thing. Your MainForm would then subscribe to both events
and do the right thing in each case.

For your simple example: setting the text box contents, I would go for
the first option: an event and a property. However, as I mentioned, it
doesn't fit all problems.

The basic idea here is to hide the structure of the user control. Say,
for example, the two buttons are two different answers to the same
question. If tomorrow you decide on a different design, and want to
show the user a radio button pair and a button to press, then what? If
you expose events like Button1Clicked and Button2Clicked then you have
to fix your MainForm, too. If, on the other hand, you expose events and
properties based on what the user control is _for_, rather than how it
is _built_, then you shouldn't have to change anything on the outside
if you change the way the user control looks.

As well, because it's the MainForm's responsibility to subscribe to the
user control's events, you can drop as many user controls on a form as
you like, or use the user control on as many forms as you like, and the
user control itself doesn't need to understand the context in which
it's being used. It just notifies "the outside world" when something
happens.
 
Bruce said:
Since your user control is (theoretically, at least) a reusable
component, it shouldn't contain a direct reference to MainForm.

The correct way to do this is using events. Your user control should
expose either one or two events depending upon what the button presses
mean.

Thanks for the explanation. Raising an event is what I need, since my MainForm will need to update the appropriate label according to which tab page is selected. I followed the code from the SDK documentation to add the events and subscriptions, but it's not working. Here's my test case:

MainForm.cs:

namespace test01 {
public partial class MainForm : Form {
public void ReceiveBIEvent(object sender, EventArgs e) {
MessageBox.Show("It worked!");
}

public void SubscribeBIEvent(BIGroup BISource) {
test01.BIGroup.SendButtonClick temp = new test01.BIGroup.SendButtonClick(ReceiveBIEvent);
BISource.BIButtonClicked += temp;
}
}
}


BIGroup.cs:

namespace test01 {
public partial class BIGroup : UserControl {
private void button1_Click(object sender, EventArgs e) {
RaiseBIEvent();
}

public delegate void SendButtonClick(object sender, EventArgs e);
public event SendButtonClick BIButtonClicked;

private void RaiseBIEvent() {
// Safely invoke an event:
SendButtonClick temp = BIButtonClicked;

if (temp != null) {
temp(this, new System.EventArgs());
}
}
}
}


It appears that the RaiseBIEvent() method can't see that MainForm has an event handler for the event -- when I step through the application with the debugger, the "if (temp != null)" exits the RaiseBIEvent() method and the event is never fired. Any ideas?
 
Where in your MainForm do you call SubscribeBIEvent() ? If it is never
called, then MainForm will never subscribe to the event, so there will
be no subscribers, and you'll see exactly the behaviour you describe.
 
Bruce said:
Where in your MainForm do you call SubscribeBIEvent() ? If it is never
called, then MainForm will never subscribe to the event, so there will
be no subscribers, and you'll see exactly the behaviour you describe.

OK, the docs never mentioned calling SubscribeBIEvent(). Now, my problem is that wherever I try to call it I get an "Object not set to an instance" error. Where and how should I call that subscribe method?
 
Ken said:
OK, the docs never mentioned calling SubscribeBIEvent(). Now, my problem is that wherever I try to call it I get an "Object not set to an instance" error. Where and how should I call that subscribe method?

If I were you I would do it the easy way, using the Visual Studio
designer.

I assume that you've dropped at least one of your BIGroup user controls
onto your MainForm using the Designer, right? So your MainForm has a
BIGroup to which to listen.

In the Designer, right-click on the BIGroup that you placed on the
MainForm, and choose Properties. At the top of the property grid there
should be a lightning bolt symbol (this is VS2003... I assume that
VS2005 is similar). Click on this, and you should see a list of names
of events that your user control can raise. Find the BIButtonClicked
event in the list.

Beside the BIButtonClicked name, if you click in the empty space you
should get a combo box arrow. If you click on it, you should see
ReceiveBIEvent as one of the choices. Choose it.

Save your MainForm and recompile. That should be it.

In case this doesn't work, or isn't clear, you can do it the
long-handed way. In the constructor for MainForm, just call
SubscribeBIEvent and pass the name of the BIGroup that you placed on
MainForm:

public MainForm()
{
... other stuff that the Designer puts in here...
SubscribeBIEvent(this.BIGroup1);
}

I'm not 100% sure of the way that VS2005 writes code using partial
classes... I'm still on VS2003, myself. However, the theory is sound:
you have to subscribe to the event in the constructor, or in some event
that happens as the form is being loaded... (the Load event handler is
another good choice).
 
Bruce said:
If I were you I would do it the easy way, using the Visual Studio
designer.

Ah, nothin's ever easy....
I assume that you've dropped at least one of your BIGroup user controls
onto your MainForm using the Designer, right? So your MainForm has a
BIGroup to which to listen.

Actually, no. Two of my reasons for using User Controls is to add them 1) automatically when the form is initialized, and 2) when the user clicks a button.

I *have* found where to call the SubscribeBIEvent() method, though: in the method I use to add the User Control. It goes something like this:

private void AddUserControl() {
Control BIGroup_tp1 = new BIGroup();
panel1.SuspendLayout();
panel1.Controls.Add(BIGroup_tp1);
// do a lot more stuff here.
panel1.ResumeLayout();
SubscribeBIEvent((BIGroup)BIGroup_tp1);
}

The final problem was a compiler error about a Contol not being a BIGroup (?!), which I solved by doing the cast. Thanks again for all your help. These User Controls have a lot of potential, but I'm finding that manipulating them through the code can be onerous.
 
Tweak your code to get rid of the casts:

private void AddUserControl() {
BIGroup BIGroup_tp1 = new BIGroup();
panel1.SuspendLayout();
panel1.Controls.Add(BIGroup_tp1);
// do a lot more stuff here.
panel1.ResumeLayout();
SubscribeBIEvent(BIGroup_tp1);
}

I think you'll find that UserControls are the easiest way to do this.
It may be difficult, but the other ways are worse. :-)

By the way, I usually don't write a whole separate method just to
subscribe to an event. As well, there's really no need for the
ReceiveBIEvent event type, since it's just a System.EventHandler at
heart. I would have written it more like this:

namespace test01 {
public partial class MainForm : Form {
private void ReceiveBIEvent(object sender, EventArgs e)
{
MessageBox.Show("It worked!");
}

private void AddUserControl() {
BIGroup BIGroup_tp1 = new BIGroup();
panel1.SuspendLayout();
panel1.Controls.Add(BIGroup_tp1);
// do a lot more stuff here.
panel1.ResumeLayout();
BIGroup.tp1.BIButtonClicked += new
System.EventHandler(ReceiveBIEvent);
}
}
}

BIGroup.cs:

namespace test01 {
public partial class BIGroup : UserControl {
private void button1_Click(object sender, EventArgs e)
{
RaiseBIEvent();
}

public event System.EventHandler BIButtonClicked;

private void RaiseBIEvent() {
// Safely invoke an event:
System.EventHandler temp = BIButtonClicked;

if (temp != null) {
temp(this, new System.EventArgs());
}
}
}
}
 
Bruce said:
Tweak your code to get rid of the casts:

Done. Thanks.
I think you'll find that UserControls are the easiest way to do this.
It may be difficult, but the other ways are worse. :-)

Yep. UserControls are a more efficient and elegant solution so far and I really want to use them, but I still have to test a couple of other use cases before I commit to using them in this app. I suspect serialization is going to be another hassle....
By the way, I usually don't write a whole separate method just to
subscribe to an event. As well, there's really no need for the
ReceiveBIEvent event type, since it's just a System.EventHandler at
heart.

I wondered about that when reading the Subscribe Event docs. It seemed overly complicated when all that was needed was something as simple as setting an event handler for, say, a TextBox.TextChanged event. I've implemented your suggestion and it's working well. Thanks again.
 
Back
Top