#c# #events #design-patterns
#c# #Мероприятия #шаблоны проектирования
Вопрос:
Я пытаюсь лучше понять, как поддерживать подписку на класс, который может меняться (стратегии изменения). Я постараюсь придерживаться этого направления, даже если примеры будут надуманными.
Предположим, что есть оболочка класса
public class Skin
{
//Raised when the form needs to turn on/off a blinking light
public event BlinkEventHandler BlinkEvent;
//The back color that forms should use
public Color BackColor{ get; protected set; }
}
При запуске приложения оно прочитает каталог, полный файлов конфигурации для разных классов оболочки. Пользователь может переключить текущий скин в любое время.
В моей текущей работе используется очень странная стратегия (IMO), которая выглядит следующим образом:
/// <summary>
/// Some class that can see when the Skin Changes
/// </summary>
public class SkinManager
{
//Raised when the Skin changes
public event SkinChangedEventHandler SkinChangedEvent;
private static Skin currentSkin;
public static Skin CurrentSkin {get;}
public SkinManager(){/* gets a skin into currentSkin */}
public void ChangeSkin()
{
//... do something to change the skin
if(SkinChangedEvent != null)
{
SkinChangedEvent(this, new SkinChangedEventArgs(/*args*/));
}
}
}
/// <summary>
/// Some form that follows the Skinning Strategy
/// </summary>
public class SkinnedForm : Form
{
private Skin skin;
public SkinnedForm()
{
skin = SkinManager.CurrentSkin;
if(skin != null)
{
skin.BlinkEvent = OnBlink;
}
SkinManager.SkinChangedEvent = OnSkinChanged;
}
private void OnSkinChanged(object sender, SkinChangedEventArgs e)
{
//unregister if we have a current skin
//the local was to ensure that the form unsubscribes
//when skin changes
if(skin != null)
{
skin.BlinkEvent -= OnBlink;
}
skin = SkinManager.CurrentSkin;
if(skin != null)
{
skin.BlinkEvent = OnBlink;
}
SkinChanged();
}
private void SkinChanged(){ Invalidate(); }
private void OnBlink(object sender, BlinkEventArgs e)
{
//... do something for blinking
}
}
Я не могу поверить, что это хорошая реализация, и вместо этого хотел бы видеть что-то вроде этого:
/// <summary>
/// Some class that can see when the Skin Changes
/// </summary>
public class SkinManager
{
//Raised when the Skin changes
public event SkinChangedEventHandler SkinChangedEvent;
//Relays the event from Skin
public event BlinkEventHander BlinkEvent;
private static Skin currentSkin;
public static Skin CurrentSkin {get;}
public SkinManager()
{
//... gets a skin into currentSkin
currentSkin.BlinkEvent = OnBlink;
}
/// <summary>
/// Relays the event from Skin
/// </summary>
private void OnBlink(object sender, BlinkEventArgs e)
{
if(BlinkEvent != null)
{
BlinkEvent(this, e);
}
}
public void ChangeSkin()
{
//... do something to change the skin
if(SkinChangedEvent != null)
{
SkinChangedEvent(this, new SkinChangedEventArgs(/*args*/));
}
}
}
/// <summary>
/// Some form that follows the Skinning Strategy
/// </summary>
public class SkinnedForm : Form
{
//Do not need the local anymore
//private Skin skin;
public SkinnedForm()
{
SkinManager.CurrentSkin.BlinkEvent = OnBlink;
SkinManager.SkinChangedEvent = OnSkinChanged;
}
private void OnSkinChanged(object sender, SkinChangedEventArgs e)
{
//Only register with the manager, so no need to deal with
//subscription maintenance, could just directly to go SkinChanged();
SkinChanged();
}
private void SkinChanged() { Invalidate(); }
private void OnBlink(object sender, BlinkEventArgs e)
{
//... do something for blinking
}
}
Я не уверен, что это понятно, но в основном существует локальная переменная, которая используется строго для того, чтобы мы отписывались от событий, прежде чем подписываться на события в новом классе. Я рассматриваю это следующим образом: мы внедрили шаблон стратегии для скининга (выберите стратегию скининга, которую вы хотите использовать, и запустите с ней), но у каждой реализации стратегии есть события, на которые мы напрямую подписываемся. Когда стратегия меняется, мы хотим, чтобы наши подписчики смотрели правильного издателя, поэтому мы используем локальные. Опять же, я думаю, что это ужасная методология.
Есть ли название для преобразования, которое я предложил, используя менеджер для отслеживания всех событий класса, которым он управляет, и передачи их, чтобы стратегия могла измениться, а подписчики продолжали прослушивать правильные уведомления о событиях? Предоставленный код был создан «на лету», когда я формировал вопрос, поэтому простите за любые ошибки.
Ответ №1:
Как правило, когда вы хотите создать прокси (оболочку) для класса, который запускает события, вам нужно отменить подписку (отсоединить) предыдущего экземпляра, поменять местами с новым, а затем подписаться (прикрепиться) к его событиям.
Допустим, ваш пользовательский интерфейс выглядит следующим образом:
interface ISkin
{
void RenderButton(IContext ctx);
event EventHandler Blink;
}
Тогда часть, в которой вы это изменяете, должна выглядеть следующим образом:
public void SetSkin(ISkin newSkin)
{
// detach handlers from previous instance
DetachHandlers();
// swap the instance
_skin = newSkin;
// attach handlers to the new instance
AttachHandlers();
}
void DetachHandlers()
{
if (_skin != null)
_skin.Blink -= OnBlink;
}
void AttachHandlers()
{
if (_skin != null)
_skin.Blink = OnBlink;
}
Полный прокси-сервер выглядел бы примерно так:
interface IChangeableSkin : ISkin
{
event EventHandler SkinChanged;
}
public class SkinProxy : IChangeableSkin
{
private ISkin _skin; // actual underlying skin
public void SetSkin(ISkin newSkin)
{
if (newSkin == null)
throw new ArgumentNullException("newSkin");
if (newSkin == _skin)
return; // nothing changed
// detach handlers from previous instance
DetachHandlers();
// swap the instance
_skin = newSkin;
// attach handlers to the new instance
AttachHandlers();
// fire the skin changed event
SkinChanged(this, EventArgs.Empty);
}
void DetachHandlers()
{
if (_skin != null)
_skin.BlinkEvent -= OnBlink;
}
void AttachHandlers()
{
if (_skin != null)
_skin.BlinkEvent = OnBlink;
}
void OnBlink(object sender, EventArgs e)
{
// just forward the event
BlinkEvent(this, e);
}
// constructor
public SkinProxy(ISkin initialSkin)
{
SetSkin(initialSkin);
}
#region ISkin members
public void RenderButton(IContext ctx)
{
// just calls the underlying implementation
_skin.RenderButton(ctx);
}
// this is fired inside OnBlink
public event EventHandler BlinkEvent = delegate { };
#endregion
#region IChangeableSkin members
public event EventHandler SkinChanged = delegate { };
#region
}
Ваша форма должна содержать только ссылку на реализацию IChangeableSkin
.
Комментарии:
1. Потрясающе, спасибо. Это именно то, что я предлагал, но хотел убедиться, что я не ошибся. Значит, это называется проксированием? По какой-то причине они называют это Provider здесь, но когда я посмотрел шаблон, он, похоже, не соответствовал.
Ответ №2:
Довольно сложно, и бремя переключения ложится на подписчика. Это не так уж хорошо.
При замене оболочек старая оболочка может удалить своих подписчиков на события и, вероятно, также присоединить их к новой оболочке.
Но более аккуратным шаблоном может быть держатель обложки, который не меняется и который предоставляет события.
Комментарии:
1. Я согласен, что это не очень хорошая ситуация с тем, что у вас есть. Отличается ли предлагаемое изменение от владельца обложки, который вы предлагаете? Нормально ли иметь менеджера также в качестве держателя?
2. Да, я думаю, ваш менеджер делает примерно то же самое.
Ответ №3:
SkinnedForm может иметь свойство типа iSkin —
public class SkinnedForm : Form
{
private ISkin _Skin;
...
}
Предоставьте это через общедоступное свойство и установите его в любой момент. Таким образом, SkinnedForm никогда не заботится о том, как работает iSkin, или о модели событий, которую он содержит. Когда вы передаете новую ссылку на класс оболочки, новое событие OnBlink автоматически вступит во владение. Классы, реализующие iSkin, должны содержать логику для OnBlink.
Затем у вас есть класс manager (не слишком далеко от того, что вы указали), который может получить ссылку на новый скин и соответствующую форму SkinnedForm. Единственной задачей менеджера является обновление свойства iSkin в SkinnedForm.
Комментарии:
1. Вау, я не знал, что сохранение локального интерфейса позволит мне управлять подписками. Чтобы проверить, вы это говорите… SkinnedForm(){_Skin = SkinManager. CurrentSkin; _Skin. BlinkEvent = OnBlink; GetDifferentSkin();} GetDifferentSkin(){_Skin = SomeOtherSkin;} После вызова GetDifferentSkin события все еще связаны?
2. Привет, не совсем. Я говорю, что в любой момент SkinnedForm должен иметь ссылку на один объект оболочки, который имеет единственную реализацию BlinkEvent. Если вы замените весь объект Skin, то нет необходимости отменять / перерегистрировать новые события. Особенно не в объекте SkinnedForm, его не должно волновать, как реализованы события оболочки, он должен знать только, что у него есть оболочка, и он может вызывать методы / вызывать события в этой оболочке.
3. Фух, на секунду подумал, что мне не хватает огромной части языка. Я провел некоторое тестирование и обнаружил, что действительно, он не выполняет автоматическую замену. Я полагаю, что ваше предложение похоже на то, что предложил Groo. Спасибо: D