Проекты

Software Abstraction Layer core API

История возникновения

Разработка архитектуры началась в 2009 году. Несколько раз был произведён полный рефакторинг основной API, добавлялись и удалялись корневые узлы системы. С начала 2012 года, система практически не менялась. Из чего следует что основа стала достаточно стабильной.

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

  1. Централизованное хранилище настроек
    Для каждого сотрудника настройки хранятся на другом сервере и по определённой роли сотрудника, те или иные настройки можно прочитать, а при расширенных правах можно и изменить.
  2. Отображение UI с определённым функционалом доступного для сотрудника согласно его роли.
    Я предполагаю использование 2 вариантов использования такого подхода:
    1. Приложение в себе содержит весь код и отображает нужные элементы меню исходя из прочитанных настроек.
      Подход содержит в себе вполне предсказуемую ошибку. При знаниях пользователя о внутреннем устройстве некоторых директориях PE файла, пользователь может не только увидеть интерфейс, который он не должен видеть, но и увидеть удалённые вызовы, что подвергнет опасности инфраструктуру серверов с БЛ, при достаточно халатном отношении к мерам безопасности.
    2. Приложение компилируется отдельно для каждой роли, при этом при компиляции, доступен максимальный код приложения. В результатирующий PE файл попадает только тот функционал, который доступен для конкретной роли. В таком случае перенос или расширение роли пользователя подразумевает под собой работу программиста. Я такого подхода не встречал, но такой подход имеет шанс на существование.

Проанализировав эти требования, мне в голову пришла идея написать некий унифицированный интерфейс, решающий в себе ряд задач:

  1. Централизованное хранилище настроек (при необходимости).
    По умолчанию, в качестве хранилища настроек используется файловая система. Но, при необходимости, можно написать плагин, взяв на основу интерфейс описанный в базовой сборке ISettingsProvider позволяющий сохранять и загружать настройки пользователей из любого удобного источника.
  2. Загрузка нужных модулей для UI и процессов по роли сотрудника.
    По умолчанию, плагины читаются из текущей директории в которой находится управляющее приложение. При необходимости, использовав интерфейс IPluginProvider можно написать расширение, которое подгружает необходимые модули из любого источника.
  3. Использование в качестве основы любой интерфейс (SDI/MDI/DialogBased).
    Для одного сотрудника, с максимальными правами и с десятком форм может быть удобно использование MDI приложения, а для сотрудника с минимальными правами и с 1/2 окнами, MDI интерфейс будет не удобен. А для программиста, использующего в качестве IDE Visual Studio, может быть удобно использовать интерфейс студии.
  4. Многоразовый код, который может использоваться не только в WinForms, но так же в ASP.NET или Win/Web сервисах.
    Такие модули как автозапуск приложения при старте системы, ограничение приложения одним экземпляром, рантайм компилятор или приложение перехватывающее SENS события могут быть подгружены из центрального хранилища для всех приложений.

Концепция

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

Минималь:

  1. Возможность использования ранее скомпилированного кода.
    • Из кода одного модуля можно обращаться к коду другого модуля.
    • Изменив зависящий модуль, все зависимые плагины получат новую версию без обязательной необходимости перекомпиляции.
    • Сохранение позиций всех элементов формы между закрытием и запуском основного приложения.
  2. Простота интеграции.
  3. Простота переноса. В качестве UI компонентов для WinForms приложений использованы классы UserControl (Использование в качестве основы класса Form не позволяют некоторые хосты в виде EnvDTE). При необходимости, код без особых затрат можно вернуть в стандартное приложение.

Архитектура

Работающее приложение представляет из себя следующий вид:

  1. Приложение (Поддерживаемые на момент написания интерфейсы: MDI/DialogBased/EnvDTE/Windows Service/ASP.NET)
  2. SAL хост, который знает как общаться с приложением и занимается подгрузкой плагинов..
  3. Плагины, которые не знают, в каком приложении они запущены, но, которые могут общаться с приложением через SAL хост.
Приложение

Приложение может являться хостом, как в случае с Flatbed.MDI, Flatbed.Dialog. Так и отдельным плагином, как в случае с Flatbed.EnvDTE

SAL хост
  • После загрузки всех плагинов в память из текущей папки провайдером плагинов по умолчанию, загружаемый хост, ищет в массиве загруженных ранее плагинов 2 типа плагина: провайдер настроек (ISettingsProvider) и провайдер плагинов (IPluginProvider). Если найден другой провайдер настроек или плагинов, то текущий провайдер становится родителем для найденного провайдера.
    • ISettingsProvider позволяет загружать настройки плагинов из разных источников. При этом, в самом хосте, по умолчанию, реализован провайдер натроек из XML.
    • IPluginProvider позволяет загружать и обновлять плагины из любого (написанного ранее кода) источника. По умолчанию, в хосте реализован провайдер плагинов, который ищет плагины в текущей папки хоста.
  • Затем производится поиск Kernel плагина. Kernel плагинов может быть несколько и каждый из них может предоставлять BLL или DAL. Так-же, Kernel плагин может влиять на UI (в случае с WinForms приложениями) и на загрузку и выгрузку других плагинов. Допустим, если загружен плагин, которого не должно быть в системе, то дальнейшую загрузку можно прекратить.
  • После загрузки и инициализации провайдеров и всех Kernel плагинов, происходит инициализация всех остальных плагинов. Каждый последующий плагин, при необходимости, ищет в списке плагинов Kernel плагин на который он ссылается (Reference). Если плагин унифицинованный, то ссылка на Kernel плагин ему не нужна.
Плагины

Если плагин ссылкается только на интерфейсы сборки SAL.Flatbed, то UI он не использует и может работать в любом окружении. Если плагин ссылается на сборку более высокого уровня, к примеру SAL.Windows (Который ссылается на SAL.Flatbed), то плагину для реализации полного функционала понадобится WinForms UI.

SAL Host может предоставлять дополнительный набор интерфейсов, который наследуется от минимально базы интерфейсов самого хоста. К примеру, для получения расширенного набора функционала, который предоставляет хост Flatbed.EnvDTE, необходимо скастовать интерфейс IHost в интерфейс IHostAddIn описанный с сборке SAL.EnvDTE.

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

Описание сборок

  • SAL.Flatbed.dll — Корневая сборка, содержащая в себе все базовые интерфейсы обеспечивающие минимальный набор интерфейсов для реализации модулей и хостов.
  • SAL.Windows.dll — Сборка для разработки компонентов для WinForms приложений
  • SAL.Web.dll — Сборка для разработки компонентов для приложений на основе ASP.NET.

Описание взаимодействия

После завершения загрузки всех плагинов, вызывается метод Boolean OnConnection(ConnectMode mode).

Взаимодействие плагинов между собой и между хостом происходит через публичные методы, свойства и события. Так-же, доступен метод Object InvokeMessage(String message, params Object[] args), которое содержится в интерфейсе IPlugin. Метод делался по аналогии с WinAPI функцией SendMessage. Он остался от одной из старых версий, всё рука не поднимается его уничтожить.

Взаимодействие плагина с хостом происходит через свойство Host доступное в базовом интерфейсе IPlugin. В случае загрузки плагина в хост, поддерживающий отображение окон, IHost можно попытаться скастовать в расширенную версию интерфейса — IHostWindows, который добавляет дополнительный функционал связанный с WinForms.

Примеры взаимодействия

Получение и загрузка настроек плагина. (При наследовании от интерфейса IPluginSettings<T>).

public class Plugin : IPlugin, IPluginSettings<PluginSettings>
{
...
	private PluginSettings _settings;
	public PluginSettings Settings
	{
		get
		{
			if(this._settings == null)
			{
				this._settings = new PluginSettings(this);
				this.Host.Plugins.SettingsProvider.LoadAssemblyParameters(this, this._settings);
			}
			return this._settings;
		}
	}
	Object IPluginSettings.Settings { get { return this.Settings; } }
...
}

Загрузка большого объекта из настроек (к примеру DataSet)

using(Stream stream = this.Plugin.Host.Plugins.SettingsProvider.LoadAssemblyBlob(this.Plugin, Constant.SettingsFileName))// Constant.SettingsFileName="DataSet.xml"
	if(stream != null)
		base.DataSet.ReadXml(stream);

Сохранение большого объекта в настройки. (Для примера DataSet)

using(MemoryStream stream = new MemoryStream())
{
	base.DataSet.WriteXml(stream);
	this.Plugin.Host.Plugins.SettingsProvider.SaveAssemblyBlob(this.Plugin, Constant.SettingsFileName, stream);
	// Constant.SettingsFileName="DataSet.xml"
}

Добавление элементов меню в хост поддерживающий набор интерфейсов SAL.Windows.

internal IHostWindows HostWindows { get { return this.Host as IHostWindows; } }//Свойство Host наследуется из интерфейса IPlugin
...
IHostWindows host = this.HostWindows;
if(host != null)
{
	IMenuItem menuTools = host.MainMenu.FindMenuItem("Tools");
	if(menuTools != null)
	{
		this.RdpClientMenu = menuTools.Create("RDP Client");
		this.RdpClientMenu.Name = "tsmiToolsRdpClient";
		this.RdpClientMenu.Click += new EventHandler(RdpClientMenu_Click);
		menuTools.Items.Insert(0, this.RdpClientMenu);
		return true;
	}
}

Открытие окна (В базе создаваемого окна должен быть класс UserControl)

internal IHostWindows HostWindows { get { return this.Host as IHostWindows; } }//Свойство Host наследуется из интерфейса IPlugin
...
IWindow wnd = this.HostWindows.Windows.CreateWindow(this, typeof(PanelRdpClient).ToString(), false, DockState.DockLeft);

Получение объектов IWindow и IPlugin в открытом окне.

public partial class PanelRdpClient : UserControl
{
	private IWindow Window { get { return (IWindow)base.Parent; } }
	private PluginRdp Plugin { get { return (PluginWindows)this.Window.Plugin; } }
...
	protected override void OnCreateControl()
	{
		this.Window.Caption = "I want cookie";

		base.OnCreateControl();
	}
...
}

Краткое описание объектов SAL.Flatbed.dll

  • IPlugin — Интерфейс, который должен быть реализован в любом плагине.
  • IPluginSettings, IPluginSettings<T> — Интерфейс для расширения плагина настройками. Содержит свойство Settings типа Object.
  • IPluginKernel — Интерфейс является маркером, что плагин является объектом предоставляющим основу для остальных плагинов.
  • IPluginProvider — Интерфейс для загрузчика плагинов из любого источника.
  • ISettingsProvider — Интерфейс для загрузчика настроек плагинов, на стороне плагинов, за загрузку отвечает интерфейс ICustomizable описанный выше.

SAL.Flatbed class diagram

Диаграмма классов сборки SAL.Flatbed

SAL.Windows class diagram

Диаграмма классов сборки SAL.Windows

SAL.Web class diagram

Диаграмма классов сборки SAL.Web

Все диаграммы классов SAL, SAL.Windows, SAL.Web, SAL.EnvDTE.

Примеры UI

Теги:

Скачать

  • 4 июля 2016 г.
    Удалены или перенесены в отдельные модули лишние объекты
  • 31 мая 2016 г.
    Публичный релиз со статьёй
  • 13 июля 2013 г.
    Небольшие изменения в типах данных.
  • 31 октября 2012 г.
    Первая публичная версия. Архив содержит 3 сборки:
    • SAL.Flatbed.dll — База
    • SAL.Windows.dll — База для WinForms
    • SAL.Web.dll — База для ASP.NET

Ссылки

Дочерние файлы