ПРИМЕЧАНИЕ. Если у вас есть доступ к C # 5.0 Unleashed , прочитайте раздел «Ограничения простого использования делегатов» в главе 18 под названием «События», чтобы лучше понять различия между ними.
Мне всегда помогает простой, конкретный пример. Так что вот один для сообщества. Сначала я покажу, как вы можете использовать только делегатов, чтобы делать то, что события делают для нас. Затем я покажу, как это же решение будет работать с экземпляром EventHandler
. А потом я объясняю, почему мы не хотим делать то, что я объясняю в первом примере. Этот пост был вдохновлен статьей Джона Скита.
Пример 1. Использование открытого делегата
Предположим, у меня есть приложение WinForms с одним раскрывающимся списком. Выпадающий список привязан к List<Person>
. Где Person имеет свойства Id, Name, NickName, HairColor. На главной форме находится пользовательский элемент управления, который показывает свойства этого человека. Когда кто-то выбирает человека в раскрывающемся списке, ярлыки в пользовательском элементе управления обновляются, чтобы показать свойства выбранного человека.
Вот как это работает. У нас есть три файла, которые помогают нам составить это:
- Mediator.cs - статический класс содержит делегаты
- Form1.cs - основная форма
- DetailView.cs - пользовательский элемент управления отображает все детали
Вот соответствующий код для каждого из классов:
class Mediator
{
public delegate void PersonChangedDelegate(Person p); //delegate type definition
public static PersonChangedDelegate PersonChangedDel; //delegate instance. Detail view will "subscribe" to this.
public static void OnPersonChanged(Person p) //Form1 will call this when the drop-down changes.
{
if (PersonChangedDel != null)
{
PersonChangedDel(p);
}
}
}
Вот наш пользовательский элемент управления:
public partial class DetailView : UserControl
{
public DetailView()
{
InitializeComponent();
Mediator.PersonChangedDel += DetailView_PersonChanged;
}
void DetailView_PersonChanged(Person p)
{
BindData(p);
}
public void BindData(Person p)
{
lblPersonHairColor.Text = p.HairColor;
lblPersonId.Text = p.IdPerson.ToString();
lblPersonName.Text = p.Name;
lblPersonNickName.Text = p.NickName;
}
}
Наконец, у нас есть следующий код в нашем Form1.cs. Здесь мы вызываем OnPersonChanged, который вызывает любой код, подписанный на делегат.
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
Mediator.OnPersonChanged((Person)comboBox1.SelectedItem); //Call the mediator's OnPersonChanged method. This will in turn call all the methods assigned (i.e. subscribed to) to the delegate -- in this case `DetailView_PersonChanged`.
}
Ok. Так вот, как бы вы работали без использования событий и только с использованием делегатов . Мы просто помещаем открытый делегат в класс - вы можете сделать его статическим или одноэлементным, или как угодно. Отлично.
НО, НО, НО, мы не хотим делать то, что я только что описал выше. Потому что открытые поля плохие по многим, многим причинам. Итак, каковы наши варианты? Как описывает Джон Скит, вот наши варианты:
- Открытая переменная делегата (это то, что мы только что сделали выше. Не делайте этого. Я только что сказал вам выше, почему это плохо)
- Поместите делегата в свойство с get / set (проблема здесь в том, что подписчики могут переопределять друг друга - чтобы мы могли подписать несколько делегатов на делегат, а затем мы могли бы случайно сказать
PersonChangedDel = null
, уничтожив все из других подписок. Другая проблема, которая остается здесь, состоит в том, что, поскольку пользователи имеют доступ к делегату, они могут вызывать цели в списке вызовов - мы не хотим, чтобы внешние пользователи имели доступ к тому, когда вызывать наши события.
- Переменная делегата с методами AddXXXHandler и RemoveXXXHandler
Этот третий вариант, по сути, дает нам событие. Когда мы объявляем EventHandler, он дает нам доступ к делегату - не публично, не как свойство, а как вещь, которую мы называем событием, имеющим только добавление / удаление аксессоров.
Давайте посмотрим, как выглядит та же самая программа, но теперь она использует событие вместо публичного делегата (я также изменил наш посредник на одноэлементный):
Пример 2. С EventHandler вместо открытого делегата
Посредник:
class Mediator
{
private static readonly Mediator _Instance = new Mediator();
private Mediator() { }
public static Mediator GetInstance()
{
return _Instance;
}
public event EventHandler<PersonChangedEventArgs> PersonChanged; //this is just a property we expose to add items to the delegate.
public void OnPersonChanged(object sender, Person p)
{
var personChangedDelegate = PersonChanged as EventHandler<PersonChangedEventArgs>;
if (personChangedDelegate != null)
{
personChangedDelegate(sender, new PersonChangedEventArgs() { Person = p });
}
}
}
Обратите внимание, что если вы нажмете F12 на EventHandler, он покажет вам, что определение является просто делегатом общего назначения с дополнительным объектом "отправителя":
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Пользовательский контроль:
public partial class DetailView : UserControl
{
public DetailView()
{
InitializeComponent();
Mediator.GetInstance().PersonChanged += DetailView_PersonChanged;
}
void DetailView_PersonChanged(object sender, PersonChangedEventArgs e)
{
BindData(e.Person);
}
public void BindData(Person p)
{
lblPersonHairColor.Text = p.HairColor;
lblPersonId.Text = p.IdPerson.ToString();
lblPersonName.Text = p.Name;
lblPersonNickName.Text = p.NickName;
}
}
Наконец, вот код Form1.cs:
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
Mediator.GetInstance().OnPersonChanged(this, (Person)comboBox1.SelectedItem);
}
Поскольку EventHandler хочет и EventArgs в качестве параметра, я создал этот класс только с одним свойством в нем:
class PersonChangedEventArgs
{
public Person Person { get; set; }
}
Надеюсь, это немного покажет вам, почему у нас есть события и как они отличаются - но функционально одинаковы - как делегаты.