Лучший способ присоединиться к событиям далеко в callstack в C #? - PullRequest
4 голосов
/ 06 декабря 2008

Какое наилучшее проектное решение для класса «верхнего уровня» присоединить к событию к классу, который может быть на 5+ уровней ниже в стеке вызовов?

Например, возможно, MainForm породил объект, а этот объект породил стек вызовов нескольких других вызовов объектов. Наиболее очевидным способом было бы связать событие по иерархии объектов, но это кажется грязным и требует много работы.

Еще одно решение, которое я видел, - это использование шаблона наблюдателя путем создания общедоступного статического объекта, который выставляет событие и действует как прокси между объектом нижнего уровня и формой верхнего уровня.

Любые рекомендации?

Вот пример псевдокода. В этом примере MainForm создает экземпляр SomeObject и присоединяется к событию. SomeObject присоединяется к объекту, который он создает, чтобы передать событие слушателю MainForm.

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;
}

Ответы [ 5 ]

4 голосов
/ 06 декабря 2008

Если ваше приложение не основано на блоках приложений составного пользовательского интерфейса, самое простое решение - поместить класс «слушателя» между главной формой и другими вашими компонентами, к которым оба класса могут легко получить доступ. Концептуально классы выложены следующим образом:

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

Вот пример кода:

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");
        }
    }
}

Эта программа выводит следующее:

FakeMainForm.MessageRecieved: ComponentClass.PretendToProcessData() was called

Этот код позволяет вам вызывать методы непосредственно для любого слушателя, независимо от того, как далеко они находятся друг от друга в стеке вызовов.

Легко переписать ваш класс Listener, так что он немного более универсален и работает с разными типами, но вы должны понять это.

0 голосов
/ 06 декабря 2008

Я бы не сказал, что это ошибка проектирования, у основной формы есть веские причины для того, чтобы слушать, что делает объект. Один из сценариев, с которыми я столкнулся, - это отображение пользователю сообщений о состоянии, чтобы указать, что делают фоновые процессы или что делают несколько элементов управления в многопоточном приложении, которое позволяет вам открывать сразу несколько экранов / «страниц».

В прикладном блоке составного пользовательского интерфейса базовый эквивалент контейнера внедрения зависимостей связывает события, когда его экземпляры объектов в одном и том же рабочем элементе (рабочий элемент является просто контейнером объекта для группы связанных пользовательских элементов управления). Это делается путем сканирования специальных атрибутов, таких как [EventPublication("StatusChanged")] для событий и [EventSubscription("StatusChanged")] для открытых методов. Одно из моих приложений использует эту функциональность, так что пользовательский элемент управления, находящийся во внутренней части приложения, может передавать информацию о состоянии (например, «Загрузка данных клиента ... 45%»), не зная, что эти данные окажутся в строка состояния главной формы.

Таким образом, UserControl может сделать что-то вроде этого:


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);
    }
}

... где класс StatusNotification имеет event с подписью типа


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

... и вышеупомянутые методы Message() и Dispose() этого класса вызывают это событие соответствующим образом. Но этот класс явно не связывал это событие ни с чем. Объектный экземпляр автоматически подключит события к любому с атрибутом подписки с тем же именем.

Итак, у MainForm есть обработчик событий, который выглядит примерно так:


[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;
   }
}

... или что-то подобное. Это сложнее, так как он будет вращаться через несколько уведомлений о состоянии в течение заданного количества секунд, поскольку несколько пользовательских элементов управления могут передавать сообщения о состоянии в одно и то же время.

Таким образом, чтобы воссоздать это поведение без фактического переключения на CAB (что, честно говоря, намного сложнее, чем я думаю, на самом деле это нужно), вы могли бы либо иметь объект MessageNotificationService, который вы передаете в приложении или что вы превращаетесь в статический / одноэлементный объект (я обычно избегаю такого подхода, так как его сложнее тестировать), ИЛИ вы можете сделать так, чтобы ваши подчиненные пользовательские элементы управления создавались фабричным классом, который выполняет для вас событие. Объекты могут регистрироваться на фабрике по атрибутам вашего собственного создания или путем явного вызова методов, которые говорят: «Эй, всякий раз, когда вы создаете объект с событием этой подписи, я хочу знать об этом».

Только будьте осторожны, чтобы любой класс, который вы реализуете, отцепил события, когда объект был удален, потому что в этом сценарии глупо легко получить что-то, что не будет собирать мусор.

Надеюсь, это поможет!

0 голосов
/ 06 декабря 2008

Моя первая мысль заключается в том, что с точки зрения вашей MainForm она не должна знать, что происходит на 5 уровней ниже. Он должен знать только о своих взаимодействиях с объектом, который он породил.

При этом, если ваша основная форма хочет выполнить какое-либо действие асинхронно, она должна быть в состоянии сделать это, вызывая метод для вызываемого объекта асинхронно.

Теперь, с точки зрения вашего порожденного объекта, если вы позволили вызывающей стороне выполнять какой-либо метод асинхронно, нет необходимости толкать модель событий дальше вниз ... просто вызывайте методы непосредственно вниз по стеку. Вы уже в другой теме.

Надеюсь, это немного поможет. Просто помните, что уровни вашего приложения должны знать только о том, что происходит на уровне непосредственно под ними.

0 голосов
/ 06 декабря 2008

WPF использует перенаправленные события . Они статичны и могут пузыриться или туннелировать вниз по дереву элементов. Я не знаю, используете ли вы WPF, но идея статических событий может вам помочь.

0 голосов
/ 06 декабря 2008

Моим первоначальным намерением было бы попытаться избежать этого, чтобы область видимости объекта имела очевидные границы. В конкретном случае с формами я бы попытался, чтобы родительская форма ребенка управляла всеми необходимыми связями с его предками. Можете ли вы быть более конкретным в своем деле?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...