Pull to refresh

Объектно-ориентированная разработка инсталлятора Gin

Reading time7 min
Views667
Ссылка на первую часть
Ссылка на вторую часть
Ссылка на третью часть

Ввод данных



Любой инсталлятор должен давать пользователю возможность вводить некоторые стартовый параметры, например, путь к папке, куда будет инсталлирована программа, строка подключения к базе данных, и т.д. Причем, хотелось бы, чтобы это были не просто текстовые поля, а поля, дающие возможность удобного вода данных. Если это путь установки программы, то помимо текстового поля должна быть кнопка «Browse…», если это строка подключения к БД, то пусть рядом будет кнопка для выбора или создания источника данных и т.д.

Реализуем формы ввода в виде команды:

public class UserInputCommand: Command
{

    public List<UserInputControl> InputControls { get; set; }
    public string FormCaption { get; set; }

    public override void Do(ExecutionContext context)
    {
        foreach (UserInputControl input in InputControls)
        {
            Control control = input.Create();
            // настраиваем контрол и добавляем на форму
            context.InputForm.Controls.Add(control);
        }
    }
}

К контексту выполнения мы добавили свойство InputControl (я пока еще не знаю, где буду его инициализировать), это контейнерный контрол, в который мы и будет добавлять пользовательские контролы.
Появился также класс UserInputControl с абстрактным методом Create:
public abstract class UserInputControl
{
    public string ResultName { get; set; }
    public int Height { get; set; }
    public abstract Control Create();
}

Это – абстрактный класс, от которого мы унаследуем все конкретные пользовательские контролы, такие как например UserInputTextBox – простое поле текстового ввода:
public class UserInputTextBox : UserInputControl
{
    public string Caption { get; set; }
    public string InitialValue { get; set; }

    public override Control Create()
    {
        TextBox textbox = new TextBox();
        return control;
    }
}

Это лишь упрощенный код. На самом деле, помимо текстового поля ввода, там еще будет Label, отображающий заголовок Caption, и эти два контрола будут помещены в Panel, который собственно и вернется в качестве результата метода Create. По этому образцу, наследуясь от UserInputControl мы будем создавать все остальные пользовательские контролы.
Плюс ко всему, форма ввода должна ожидать окончания пользовательского ввода, которое я реализовал при помощи запуска основного потока управления инсталлятора в отдельном потоке с продолжением выполнения по клику на кнопке Next.
Сериализация
Я выбрал XML-формат сериализации, потому что он хорошо поддерживается платформой NET. Я буду использовать класс System.Xml.Serialization.XmlSerializer, из-за простоты его использования, сериализация объекта в нем требует написания всего трех строчек кода, при этом дополнительная гибкость(если она требуется) достигается использованием атрибутов из пространства имен System.Xml.Serialization.
Вот этот код:
// сериализация
XmlSerializer ser = new XmlSerializer(typeof(T));
FileStream  stream = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write);
ser.Serialize(stream, obj);

//десериализация
XmlSerializer ser = new XmlSerializer(typeof(T));
stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
result = (T)ser.Deserialize(stream);

Как видим, в качестве входных аргументов для него служат: путь к файлу, куда будет записан сериализованный объект; сам объект; и тип сериализуемого объекта. Этот код отлично подходит для сериализации не унаследованных от других классов. Но если мы собираемся сериализовать, например, класс CreateFile, иерархия наследования которого такая CreateFile < — TransactionalCommand < — Command, то при T=Command, сериализатор сообщит нам о необходимости использовать атрибут XmlInclude, указывающий сериализатору, все возможные вложенные и родительские классы, используемый в сериализуемом объекте. И это было бы не так проблематично, если бы атрибуты эти не нужно было применять к базовому классу Command, указав для него по одному атрибуту XmlInclude Для всех его возможных наследников. А так как я предполагаю, что пользователи будут наращивать коллекцию команд в виде плагинов, не имея доступа к исходному коду класса Command, это значит, что сериализатор не сможет сериализовать все эти добавленные пользователями классы. К счастью, выход есть. Все Included-типы можно задавать не только атрибутами, но и прямыми аргументами конструктора XmlSerializer, в который вторым аргументов можно подать массив included-типов Type[].
Type[] types;
XmlSerializer ser = new XmlSerializer(typeof(T), types);

А это значит, что при создании экземпляра сериализатора, нам необходимо обладать информацией обо всех используемых типах. То есть некоторой метаинформацией об инсталляторе.
Плагины и метаданные
Я введу для этого класс GinMetaData
public class GinMetaData
{
    public List<ExternalCommand> Commands { get; private set; }
    public Type[] IncludedTypes { get; private set; }

    public static GinMetaData GetInstance();
    public void Plugin(string folderPath)
    public ExternalCommand GetCommandByName(string name)
}

Предполагаю, что в приложении всегда будет существовать только один экземпляр этого класса, а значит реализуем его как синглтон. При этом в свойстве IncludedTypes будут содержаться все типы, который могут использоваться сериализатором, причем типы как из основной библиотеки инсталлятора, так и из всех его плагинов. Для подключения к метаданным новых плагинов, используется метод Plugin, принимающий на входе путь к папке с плагинами. Если вызвать его несколько для разных папок, то все NET-библиотеки будут подключены к приложению в виде плагинов(если конечно они содержат в себе новые команды и вспомогательные классы).
Ссылки на все доступные инсталлятору команды загружаются в список GinMetaData.Commands. Для этого я создал в классе GinMetaData метод LoadCommandsFrom(Assembly):
private void LoadCommandsFrom(Assembly assembly)
{
    foreach (Type type in assembly.GetTypes())
    {
        if (ExternalCommand.ContainsCommand(type))
        {
            ExternalCommand cmd = new ExternalCommand(type);
            Commands.Add(cmd);
            _includedTypes.Add(cmd.CommandType);
        }
    }
}

Осталось лишь рассмотреть класс ExternalCommand, используемый в этом коде. Вот его интерфейс:
public class ExternalCommand
{
    public ExternalCommand(Type type)

    public Type CommandType { get; private set; }
    public Command Instance { get; private set; }
    public PropertyInfo[] Properties { get; private set; }
    public ConstructorInfo Constructor { get; private set; }
    public CommandMetadata Metadata { get; private set; }

    public static bool ContainsCommand(Type type)
    public object GetProperty(string propertyName)
    public void SetProperty(string propertyName, object value)
    public ExternalCommand Clone()
}

Каждый экземпляр ExternalCommand это — по сути, обертка вокруг экземпляра каждого из классов – потомков класса Command. У класса ExternalCommand есть один конструктор с аргументом типа Type – типом, загруженным из NET-сборки. Так как в подключаемых сборках в общем случае могут быть не только команды, но и любые вспомогательные классы, то перед попыткой создать экземпляр Command из типа, загруженного из сборки, нужно проверить, является ли загружаемый тип валидной командой. Статический метод ContainsCommand(Type) как раз и проверяет загружаемый тип на соответствие всем формальным требованиям – тип должен наследоваться от Command или от любого его наследника, тип не должен быть абстрактным, тип должен иметь конструктор по умолчанию(без него не работают сериализаторы), кроме того тип не должен быть помечен атрибутом GinIgnoreType(я ввел атрибут специально, чтобы дать возможность разработчикам плагинов помечать те типы, которые не должны экспортироваться из плагина в инсталлятор).
Свойтсво CommandType хранит объект Type рефлексии загруженной команды, в свойстве Instance хранится экземпляр загруженной команды, созданный при помощи конструктора Constructor. Свойство Properties предоставляет массив свойств команды. Так как экземпляр команды хранится в ExternalCommand в виде ссылки на Command – родительский класс всех команд, интерфейс которого имеет только метод Do() и ничего больше, а значит и свойства(properties) этого экземпляра недоступны напрямую, поэтому все свойства я экспортировал напрямую в виде массива PropertyInfo. Но и это свойство нужно в основном только для перечисления свойств. Чтобы Устанавливать и считывать каждое конкретное свойство, у класса ExternalCommand есть два метода: GetProperty(string key), и SetProperty(string key, object value).
Свойство Metadata – это любые метаданные о загруженной команде. Я подразумевал, что эти метаданные будут использоваться для отображения команд в интерфейсе визуального конструктора пакетов. Он не является темой данной статьи, но подразумевать его существование нужно. В метаданных команды можно хранить такие параметры команды, как ее название, ее описание, способ отображения в конструкторе пакетов, и т.д. На данный момент метаданные команды содержат всего два параметра: название команды, ее описание и наименование группы команд:
public class CommandMetadata
{
    public string Name { get; set; }
    public string Desription { get; set; }
    public string Group { get; set; }
}

Группа команд, это текстовая строка – название группирующего узла команды в интерфейсе конструктора пакетов. Нужна для структуризации большого количества команд в списке по группам, таким как, например: операции с файлами, управление IIS, SQL-команды, структурирующие команды. И другие группы.
Метаданные присоединяются к команде при помощи атрибутов. Пока у меня есть только один атрибут метаданных команды GinNameAttribute, вот его описание:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class GinNameAttribute : Attribute
{
    public string Name { get; set; }
    public string Description { get; set; }
    public string Group { get; set; }
}

Он применяется к классу команды, например вот так:
[GinName( Name = "ВыполнитьЕсли-Иначе", Description = "Условный оператор if-then-else", Group = "Управление пакетом")]
public class ExecuteIf : TransactionalCommand, IContainerCommand
{
  // ……
}

При загрузке каждой команды в экземпляр ExternalCommand, из команды считываются также ее атрибуты, в том числе и атрибут GinNameAttribute, который преобразуется в затем в экземпляр класса CommandMetadata.
Исходный код инсталлятора, а также три типовых сценария его применения (создание пакета, выполнение и откат) я выложил в репозитории на google-code. Вы можете использовать его в своих целях. Думаю, что это был последний пост на тему проектирования инсталлятора.
Tags:
Hubs:
Total votes 3: ↑3 and ↓0+3
Comments0

Articles