[...]
My requirements...
1. Form basically just contains the click events for my buttons at the
moment, which control what thread B does.
2. A, B, C, D and E need to use X to log information to a control in
Form.
3. C needs to disable a button in Form.
4. D needs to update some status bar text in Form.
5. D needs to update a log in Form. (Different than the log that X
updates.)
Now my question: What is it that I need to meet my requirements? When
I ask 'what ' it is that I need to meet my requirements, I am really
just trying to ask what I should be searching for. I do not understand
the correct approach to implement this. [...]
Part of the problem is that in truth, there is no one right way. .NET is
a complex environment, Forms more complexity on top of that, and the
environment offers a wide variety of possible routes to working code. So
you have the difficult task of selecting one useful, efficient, and
maintainable solution from all the possibilities that exist.
That said, it seems to me that for numbers 2 through 5, you should
consider using the same approach used to deal with #1: create events on
the classes, and respond to them appropriately.
It's not clear from your post what "information" is being generated by
classes A through E. However, assuming they can be represented as an
event, raised whenever the information needs to be presented to something
(e.g. a logging class), then you can just subscribe to the events in each
class and log them as you see fit (or not at all, if you reuse the class
in a scenario where logging isn't needed).
Of course, even having decided to use events, there is still the question
of how to expose them. My own initial preference would be to extend the
event subscription to follow the class containment hierarchy. That is,
your Form sub-class would subscribe only to an event or events in class
A. Class A would in turn subscribe to events in classes B and C, and then
feed the information from those events back to the event or events it
exposes.
Again, without specifics, I can't give a specific suggestion. But for
example, you might be logging progress data being emitted from each class
and thread. If so, class A could expose a single event through which all
progress data is exposed, or it could expose separate events for each of
classes A, B, and C, and possibly the threads D and E (which in your
question, I would have called T1 and T2, to make more clear the
distinction between the threads and the classes in the discussion).
If class A exposes just a single event, then the event could provide some
way to indicate the source of the event, so that the consumer of the event
could tell where the event came from. This could as simple as an event
that emits a simple string, and to which class A prepends something like
"B: " or "C: ", etc. to indicate the source, or the event could emit a
more complex event data type as one of its arguments in which the sender
is indicated (either via reference or some other description).
If it turns out that class A never will have any need whatsoever to
monitor events in classes B and C, and if classes B and C are exposed to
your Form sub-class (for example, the Form sub-class actually creates
those objects and then passes them to class A for use), then it could
actually make some sense to not chain the events, and just have your Form
sub-class subscribe directly to the events in all three classes.
As you can see, even using an event here, there are lots of design choices
to be made. Not only do you have to figure out the above, there is also
the question of who is actually subscribing to the event, and how that
subscription is done.
One example might be that your class X subscribes to the event(s), and is
somehow given a reference to the control where it's supposed to log the
information. Or, class X could itself be some kind of event aggregator,
where you have code in your Form sub-class that subscribes to an event in
class X, and in the handler you update the control in question. Or you
could just skip having class X altogether, and just have your Form
sub-class code subscribe to the logging-related events in classes A, B,
and C, and do the appropriate thing in the handler.
Requirements 3 through 5 can be solved similarly. In those cases, the
event would be exposed in class A, just as the more general event
described above would be. Then you can have code in your Form sub-class
that handles the appropriate behavior.
Class A need not, nor should not, really know anything about the form.
Instead, for example, for requirement #3, class A can expose an event that
indicates whatever state change is related to the button being enabled or
disabled. In other words, class A doesn't enable or disable the button;
it simply provides a way for other code that is more directly tied to the
button to observe when the button should be enabled or disabled.
Note that from class A's point of view, this event may have absolutely
nothing to do with "enable" or "disable", in the sense that the Form
sub-class understands it. You should name the event so that it describes
its relationship to class A, and not to how it expects clients to use it.
So, for example, suppose the button is supposed to be disabled when some
thread in class A starts up. You might name the event
"WorkerRunningChanged" and expose a property named "WorkerRunning" that
can be examined when the event is raised, or just have two events, named
"WorkerStarted" and "WorkerStopped".
Either way, you leave it up to the subscriber to decide what the
appropriate response is. The Form sub-class may want to disable/enable a
button, but a logging class might just write out information about the
state-change, a status indicator might just change color rather than
becoming disabled/enabled, some other class might start or stop processing
based on the change, etc. No matter what the need of the client code, the
event in the class works fine, because it's declared relative to its
relationship with the class that exposes the event, rather than assuming
some specific usage on the part of the client of that class.
Requirements 4 and 5 sound a lot like the logging requirement in #2, so I
think most if not all of the discussion above with respect to #2 should
apply to numbers 4 and 5 as well.
Of course, to further complicate things, you could in fact embed the
logging code in each class. This is often done with debugging-only type
code. If the events don't represent any specifically useful aspect of the
public API you intend for each class to expose, a static class called at
appropriate places in each of the other classes could in fact be just the
right approach, rather than exposing events on each class.
In other words, I prefer events as a general rule. But if you can't
justify the event as an integral part of a class's public API, a different
approach is warranted.
[...]
One solution that worked for requirements 3, 4 and 5 were to pass the
Form's 'this' instance into A, and then continue to pass the Form
instance into C through the constructors and had public methods in
Form that modified the controls as necessary. I read online that said
solution is considered improper.
"Improper" can be taken strongly. It's not an entirely unreasonable
solution. But it does cause the classes to be tightly integrated when
there's no clear need to write them that way. The less integration
between the classes, the better IMHO, because it allows you to maintain
one class without having to worry too much about how changes in that class
will affect other classes.
Instead, with an event-driven approach, you define a clear public API that
is not dependent on the specific classes themselves. That way, you can
easily make changes in one area without being affected by what's going on
in the other classes.
Finally, note that events are not the only way to allow classes to be
connected loosely with each other in this way. But in .NET, it's probably
the most common technique you'll find. Even in other languages, the basic
idea is the same, even if the specifics of the implementation are not
(i.e. in general, this sort of event-driven processing can add a lot of
flexibility, and even in a language without function pointers or events,
like Java, you still see the same basic concept being used).
Pete