tags:

views:

249

answers:

5

What is the best design decision for a 'top-level' class to attach to an event to a class that may be '5+ layers down in the callstack?

For example, perhaps the MainForm has spawned an object, and that object has spawned a callstack of several other object calls. The most obvious way would be to chain the event up the object hierarchy, but this seems messy and requires a lot of work.

One other solution ive seen is to use the observer pattern by creating a publically accessible static object which exposes the event, and acts as a proxy between the bottom-level object, and the top-level 'form'.

Any recommendations?

Here's a pseudo-code example. In this example, the MainForm instantiates 'SomeObject', and attaches to an event. 'SomeObject' attaches to an object it instantiates, in an effort to carry the event up to the MainForm listener.

class Mainform
{
   public void OnLoad()
   {
      SomeObject someObject = new SomeObject();
      someObject.OnSomeEvent += MyHandler;
      someObject.DoStuff();
   }

   public void MyHandler()
   {
   }
}



class SomeObject
{
   public void DoStuff()
   {
      SomeOtherObject otherObject = new SomeOtherObject();
      otherObject.OnSomeEvent += MyHandler;
      otherObject.DoStuff();
   }


   public void MyHandler()
   {
      if( OnSomeEvent != null )
          OnSomeEvent();
   }


   public event Action OnSomeEvent;
}
A: 

My initial intention would be to try and avoid that, so that an object's scope has obvious boundaries. In the particular case of Forms, I would attempt to have the child's parent form manage all required communications withs its ancestors. Can you be more specific about your case?

eulerfx
A: 

My first thought is that from your MainForm's perspective, it should have no idea what is going on 5 levels down. It should only know about its interactions with the object that it spawned.

With that, if you main form wants to perform some action asynchronously, it should be able to do that by calling a method on the spawned object asynchronously.

Now from your spawned object's point of view, if you allowed your caller to perform some method asynchronously, there's no need to push the event model further down... just call the methods directly down the stack. You're already on another thread.

Hopefully that helps a little. Just remember the levels of your app should only be aware of what goes on in the level immediately below them.

Rob
I dont want my MainForm to 'perform an action asynchronously', I want it to attach to updates from an object it instantiated, without the object it instantiated having to form an object hierarchy 'event-chain'.
A: 

WPF uses routed events. These are static and can bubble up or tunnel down the element tree. I don't know if you are using WPF, but the idea of static events might help you out.

Sorskoot
A: 

I wouldn't say this is a design fault, there are valid reasons for the main form to want to listen to what an object is doing. One scenario I've encountered is displaying status messages to the user to indicate what background processes are doing, or what multiple controls are doing in a multi-threaded app that lets you have multiple screens/"pages" open at once.

In the Composite UI Application Block, the basic equivalent of a dependency injection container wires up events when its instantiating objects in the same work item (a work item is just an object container for a group of related user controls). It does this by scanning for special attributes such as [EventPublication("StatusChanged")] on events and [EventSubscription("StatusChanged")] on public methods. One of my applications uses this functionality so that a user control instantiated way down in the innards of the application can broadcast status information (such as "Loading customer data...45%") without knowing that that data is going to end up in the main form's status bar.

So a UserControl can do something like this:


public void DoSomethingInTheBackground()
{
    using (StatusNotification sn = new StatusNotification(this.WorkItem))
    {
        sn.Message("Loading customer data...", 33);
        // Block while loading the customer data....
        sn.Message("Loading order history...", 66);
        // Block while loading the order history...
        sn.Message("Done!", 100);
    }
}

...where the StatusNotification class has an event with the a signature like


[EventPublication("StatusChanged")]
public event EventHandler<StatusEventArgs> StatusChanged;

... and the above Message() and Dispose() methods on that class invoke that event appropriately. But that class didn't explicitly have that event hooked up to anything. The object instantiator will have automatically hooked up the events to anybody with a subscription attribute of the same name.

So the MainForm has an event handler that looks something like this:


[EventSubscription("StatusChanged", ThreadOption=ThreadOption.UserInterface)]
public void OnStatusChanged(object sender, StatusEventArgs e)
{
   this.statusLabel.Text = e.Text;
   if (e.ProgressPercentage != -1)
   {
      this.progressBar.Visible = true;
      this.progressBar.Value = e.ProgressPercentage;
   }
}

... or some such. It's more complicated than that since it will rotate through multiple status notifications for a given number of seconds since multiple user controls can be broadcasting status messages around the same time.

So to recreate this behavior without actually switching over to CAB (which, to be honest, is much more complicated than I think it really needs to be), you could either have a MessageNotificationService object that you pass around your application or that you turn into a static/singleton object (I usually avoid this approach since it's harder to test), OR you could have you sub usercontrols be instantiated by a factory class that does the event wiring up for you. Objects could register with the factory by attributes of your own creation or by explicitly calling methods that say "hey, anytime you create an object with an event of this signature, I want to know about it."

Just be careful to have whatever class you implement unhook the events when an object gets disposed because it's stupid easy in this scenario to end up with something that won't get garbage collected.

Hope this helps!

Nicholas Piasecki
An elegant IoC solution you've got there. Maybe a bit more advanced of an answer than I was hoping for, but a cool idea. Thanks for your contribution! =)
+3  A: 

If your application isn't based on Composite UI Application Blocks, the easiest solution is to put a "listener" class between Main form and your other components which both classes can easily access. Conceptually, the classes are laid out as follows:

     ----------         ----------------
    | MainForm |       | Some Component |
      ---------         ----------------
          |                    |
      Hooks onto            Notifies
          |                    |
           \                  /
            -----------------
           | Proxy Notifier  |
            -----------------

Here's some example code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            FakeMainForm form = new FakeMainForm();
            form.CreateComponentAndListenForMessage();
            Console.ReadKey(true);
        }
    }

    class FakeMainForm
    {
        public FakeMainForm()
        {
            Listener.AddListener(MessageRecieved);
        }

        void MessageRecieved(string msg)
        {
            Console.WriteLine("FakeMainForm.MessageRecieved: {0}", msg);
        }

        public void CreateComponentAndListenForMessage()
        {
            ComponentClass component = new ComponentClass();
            component.PretendToProcessData();
        }
    }

    class Listener
    {
        private static event Action<string> Notify;

        public static void AddListener(Action<string> handler)
        {
            Notify += handler;
        }

        public static void InvokeListener(string msg)
        {
            if (Notify != null) { Notify(msg); }
        }
    }

    class ComponentClass
    {
        public void PretendToProcessData()
        {
            Listener.InvokeListener("ComponentClass.PretendToProcessData() was called");
        }
    }
}

This program outputs the following:

FakeMainForm.MessageRecieved: ComponentClass.PretendToProcessData() was called

This code allows you to invoke methods directly on any listener, no matter how far apart they are in the call stack.

Its easy to rewrite your Listener class so that its a little more generic and works on different types, but you should get the idea.

Juliet
Good answer! Thank you.