logo

[ELMA3] Компонентная модель

Общая информация

Компонентная модель позволяет расширять произвольные части системы. Например: добавлять пункты меню, кнопки, производить какую-либо обработку данных и т.д.

В рамках компонентной модели выделим несколько понятий:

  • Расширяемый модуль – модуль системы, который использует определенный набор точек расширения;
  • Точка расширения – произвольный интерфейс с набором свойств/методов, который помечен атрибутом EleWise.ELMA.ComponentModel.ExtensionPointAttribute;
  • Компонент – экземпляр класса, реализующего точку расширения (интерфейс), и помеченный атрибутом ComponentAttribute. Для одной точки расширения может быть несколько реализаций (компонентов).

Для инициализации и работы с ядром системы существует менеджер компонентов – класс EleWise.ELMA.ComponentModel.ComponentManager. Он позволяет инициализировать компонентную модель, регистрировать и получать компоненты. При запуске системы менеджер компонентов загружает сборки, помеченные атрибутом EleWise.ELMA.ComponentModel.ComponentAssemblyAttribute и обрабатывает их на наличие компонентов.

Основные классы для работы с компонентной моделью расположены в сборке EleWise.ELMA.SDK в пространстве имен EleWise.ELMA.ComponentModel.

Типы, используемые в компонентной модели

  • • ExtensionPointAttribute – атрибут для объявления интерфейса-расширения;
    • ComponentManager – менеджер доступа к загруженным компонентам;
    • ComponentAssemblyAttribute – атрибут для объявления сборки, содержащей компоненты;
    • ComponentAttribute – атрибут для объявления компонента;
    • IInitHandler – точка расширения для обработки загрузки компонентов.

Создание точки расширения

Для создания точки расширения необходимо создать интерфейс и пометить его атрибутом EleWise.ELMA.ComponentModel.ExtensionPointAttribute.

namespace EleWise.ELMA.ComponentModel
{

/// <summary>
/// Атрибут интерфейса точки расширения
/// </summary>
[AttributeUsage(AttributeTargets.Interface)]
public class ExtensionPointAttribute : Attribute
{

/// <summary>
/// Точка расширения с типом жизненного цикла Application и регистрацией экземпляров компонентов
/// </summary>
public ExtensionPointAttribute();

/// <summary>
/// Точка расширения с указанным типом регистрации компонентов (регистрация типов компонентов, либо экземпляров компонента)
/// </summary>
/// <param name="createInstance">Если false, то регистрируются только типы компонентов, реализующих данную точку расширения</param>
public ExtensionPointAttribute(bool createInstance);

/// <summary>
/// Точка расширения с указанным типом жизненного цикла
/// </summary>
/// <param name="serviceScope">Тип жизненного цикла компонентов, реализующих данную точку расширения</param>
public ExtensionPointAttribute(ServiceScope serviceScope);

/// <summary>
/// Создавать ли экземпляры компонентов
/// </summary>
/// <remarks>
/// True, если нужно создать экземпляры (после загрузки они доступны через метод IComponentManager.GetExtensionPoints).
/// False, если нужно загружать только их типы (после загрузки они доступны через метод IComponentManager.GetExtensionPointTypes).
/// </remarks>
public bool CreateInstance { get; }

/// <summary>
/// Контекст, в котором будут зарегистрированы и созданы компоненты, реализующие точку расширения
/// Applcation - регистрация на уровне приложения (до инициализации IInitHandler.Init), один экземпляр на приложение
/// Shell - регистрация уровне контейнера (контейнер пересоздается после включения/отключения расширений), один экземпляр на контейнер
/// Transient - регистрация уровне контейнера, экземпляр создается на пождому запросу из контейнера
/// UnitOfWork - регистрация уровне контейнера, экземпляр создается на каждый UnitOfWork (в Web сонтексте на каждый HTTP запрос)
/// </summary>
public ServiceScope ServiceScope { get; }
}

}

Пример:

/// <summary>
/// Точка расширения для меню
/// </summary>
[ExtensionPoint]
public interface IMenuExtension
{

/// <summary>
/// Получить список пунктов меню.
/// </summary>
MenuItem[] GetMenuItems(); 

}

Создание компонента

Для создания компонента нужно проверить наличие у сборки, в которой он создается, атрибута EleWise.ELMA.ComponentModel.ComponentAssemblyAttribute. Если его нет, нужно прописать.

[assembly: EleWise.ELMA.ComponentModel.ComponentAssembly]

Затем создать класс, реализовать интерфейс точки расширения и пометить атрибутом EleWise.ELMA.ComponentModel.ComponentAttribute.

namespace EleWise.ELMA.ComponentModel
{

/// <summary>
/// Атрибут компонента, реализующего точки расширений
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false)]
public class ComponentAttribute : Attribute
{

/// <summary>
/// Порядок, в котором выстраивается список компонентов, реализующих определенную точку расширения
/// </summary>
[DefaultValue(0)]
public int Order { get; set; }

/// <summary>
/// Автоматически инициализировать значения свойств (через контейнер Autofac)
/// </summary>
[DefaultValue(true)]
public bool InjectProerties { get; set; }

/// <summary>
/// Применять перехватчики методов к классу
/// </summary>
[DefaultValue(false)]
public bool EnableInterceptiors { get; set; }

}

}

Пример:

/// <summary>
/// Компонент, реализующий точку расширения IMenuExtension
/// </summary>
[Component]
public class CustomMenuExtension : IMenuExtension
{

public MenuItem[] GetMenuItems()
{
...
}

}

Менеджер компонентов

Менеджер компонентов создается при запуске системы и может быть получен после его создания через статическое свойство ComponentManager.Current.

namespace EleWise.ELMA.ComponentModel
{ /// <summary> /// Менеджер компонентов /// </summary> public class ComponentManager : IComponentManager, IDisposable { /// <summary> /// Этап жизненного цикла /// </summary> public enum LifetimeStage { /// <summary> /// До начала инициализации /// </summary> BeforeInit, /// <summary> /// В момент вызова IInitHandler.Init /// </summary> Initializing, /// <summary> /// В момент вызова IInitHandler.InitComplete /// </summary> InitCompleting, /// <summary> /// После инициализации /// </summary> Initialized, /// <summary> /// Уничтожен /// </summary> Disposed } /// <summary> /// Получить текущий менеджер /// </summary> public static ComponentManager Current { get; } /// <summary> /// Инициализирован или нет /// </summary> public static bool Initialized { get; } /// <summary> /// Текущий контейнер IoC (доступен только в процессе начала инициализации - в методе IInitHandler.Init) /// </summary> public static ContainerBuilder Builder { get; } /// <summary> /// Этап жизненного цикла /// </summary> public LifetimeStage Stage { get; } /// <summary> /// Зарегистрировать существующий компонент. Метод доступен на этапах BeforeInit и Initializing. /// </summary> /// <param name="component">Компонент</param> public ComponentManager RegisterComponent(object component); /// <summary> /// Зарегистрировать сборку, в которой будет искаться компоненты. Метод доступен на этапах BeforeInit и Initializing. /// </summary> /// <param name="assembly">Сборка</param> public ComponentManager RegisterAssembly(Assembly assembly); /// <summary> /// Возвращает компонент определенного типа. Метод доступен на этапах Initializing, InitCompleting и Initialized. /// </summary> /// <typeparam name="T">Тип расширения</typeparam> /// <returns>Экземпляр расширения</returns> public T GetExtensionPointByType<T>(); /// <summary> /// Возвращает компонент, определенного типа. Метод доступен на этапах Initializing, InitCompleting и Initialized. /// </summary> /// <param name="type">Тип расширения</param> /// <returns>Экземпляр расширения</returns> public object GetExtensionPointByType(Type type); /// <summary> /// Возвращает компоненты, реализующие интерфейс-расширение. Метод доступен на этапах Initializing, InitCompleting и Initialized. /// </summary> /// <typeparam name="T">Тип интерфейса расширения</typeparam> /// <returns></returns> public IEnumerable<T> GetExtensionPoints<T>(); /// <summary> /// Возвращает компоненты, реализующие интерфейс-расширение. Метод доступен на этапах Initializing, InitCompleting и Initialized. /// </summary> /// <param name="type">Тип интерфейса расширения</param> /// <returns></returns> public IEnumerable<object> GetExtensionPoints(Type type); /// <summary> /// Возвращает типы компонентов, реализующих интерфейс-расширение. /// </summary> /// <param name="type">Тип интерфейса расширения</param> /// <returns>Список типов компонентов. Если не найдены - возвращается пустой список.</returns> public IEnumerable<Type> GetExtensionPointTypes(Type type); /// <summary> /// Возвращает типы компонентов, реализующих интерфейс-расширение. /// </summary> /// <typeparam name="T">Тип интерфейса расширения</typeparam> /// <returns>Список компонентов</returns> public IEnumerable<Type> GetExtensionPointTypes<T>(); /// <summary> /// Возвращает типы компонентов, реализующих интерфейс-расширение. /// </summary> /// <param name="type">Тип интерфейса расширения</param> /// <returns>Список типов компонентов. Если не найдены - возвращается пустой список.</returns> public Type[] GetExtensionPointTypesArray(Type type); /// <summary> /// Получить типы, реализующие интерфейс IXsiType. /// </summary> /// <returns></returns> public Type[] GetXsiTypes(); /// <summary> /// Получить массив всех компонентов, зарегистрированных в менеджере. Метод доступен на этапах Initializing, InitCompleting и Initialized. /// </summary> /// <returns>Массив компонентов.</returns> public object[] GetComponents(); } }

Процесс инициализации менеджера компонентов выглядит следующим образом:

  1. Загружаются все сборки (*.dll и *.exe), находящиеся в папке запускаемого приложения (веб-приложение или дизайнер ELMA).
  2. Выбираются сборки, имеющие атрибут ComponentAssemblyAttribute, а также добавленные через метод ComponentManager.RegisterAssembly.
  3. Из данных сборок выбираются классы, помеченные атрибутом ComponentAttributeилиServiceAttribute. Также выбираются объекты, добавленные через метод ComponentManager.RegisterComponent.
  4. В контейнере Autofac регистрируются классы выбранные на этапе 3:
    • классы с атрибутом ComponentAttribute регистрируются с типом класса, а также типами реализуемых точек расширения;
    • классы с атрибутом ServiceAttribute регистрируются с типом класс, а также всеми реализуемыми интерфейсами.
  5. Выбираются компоненты, реализующие точку расширения IInitHandler. Для каждого из них вызывается метод Init().
  6. Обновляется контейнер Autofac компонентами, зарегистрированными на шаге 5.
  7. Выбираются компоненты, реализующие точку расширения IInitHandler. Для каждого из них вызывается метод InitComplete().
  8. Инициализация завершена.

Точка расширения IInitHandler

Системная точка расширения EleWise.ELMA.ComponentModel.IInitHandler позволяет подписываться на события начала (Init) и окончания (InitComplete) инициализации менеджера компонентов.

namespace EleWise.ELMA.ComponentModel
{

/// <summary>
/// Интерфейс компонента, поддерживающего методы инициализации
/// </summary>
[ExtensionPoint]
public interface IInitHandler
{

/// <summary>
/// Начало инициализации (могут использоваться свойства ComponentManager.Current и ComponentManager.Builder)
/// </summary>
void Init();

/// <summary>
/// Завершение инициализации (доступен Locator)
/// </summary>
void InitComplete();

}

}

В методе Init можно работать с текущим менеджером компонентов - статическим свойством ComponentManager.Current, а также построителем контейнера Autofac статическим свойством ComponentManager.Builder (см. статью Архитектура ядра системы).

В методе InitComplete можно работать с текущим менеджером компонентов, а также локатором служб Locator (см. статью Архитектура ядра системы).

Получение компонентов

Для получения компонентов, реализующих определенную точку расширения (допустим, IMenuExtension), существуют 3 способа.

Способ 1. Автоматически инициализируемые свойства (Auto-injected)

Если объект, в котором требуется получить компоненты (допустим, MenuController), помещен в контейнер Autofac (см. статью Архитектура ядра системы), то необходимо объявить свойство.

public class MenuController : Controller
{
...

public IEnumerable<IMenuExtension> MenuExtensions { get; set; }

...
}

При создании объекта MenuController свойство MenuExtensions будет заполнено автоматически, если зарегистрирован хотя бы один компонент с реализацией точки расширения IMenuExtension. Если не зарегистрирован ни один, то в свойстве будет Null.

Способ 2. Использование менеджера компонентов (ComponentManager)

using EleWise.ELMA.Services;

public class SomeClass
{

public void SomeMethod()
{
var menuExtensions = ComponentManager.Current.GetExtensionPoints<IMenuExtension>();
if (menuExtensions != null)
{
...
}
}

}

Способ 3. Использование локатора служб (Locator)

Описание класса Locator см. в статье Архитектура ядра системы

using EleWise.ELMA.Services;

public class SomeClass
{

public void SomeMethod()
{
var menuExtensions = Locator.GetService<IEnumerable<IMenuExtension>>();
if (menuExtensions != null)
{
...
}
}

}

Смотри также