Object Oriented Gin Installer Development

    Link to the first part
    Link to the second part
    Link to the third part

    Data input



    Any installer should give the user the ability to enter some start parameters, for example, the path to the folder where the program will be installed, the connection string to the database, etc. Moreover, I would like it to be not just text fields, but fields that enable convenient data water. If this is the installation path of the program, then in addition to the text field there should be a button “Browse ...”, if it is a connection string to the database, then let there be a button next to it to select or create a data source, etc.

    We implement input forms as a command:

    public class UserInputCommand: Command
    {
        public List 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);
            }
        }
    }
    

    We added the InputControl property to the execution context (I still don’t know where I will initialize it), this is a container control, in which we will add user controls.
    The UserInputControl class also appeared with the Create abstract method:
    public abstract class UserInputControl
    {
        public string ResultName { get; set; }
        public int Height { get; set; }
        public abstract Control Create();
    }
    

    This is an abstract class from which we will inherit all specific user controls, such as for example UserInputTextBox - a simple text input field:
    public class UserInputTextBox : UserInputControl
    {
        public string Caption { get; set; }
        public string InitialValue { get; set; }
        public override Control Create()
        {
            TextBox textbox = new TextBox();
            return control;
        }
    }
    

    This is just simplified code. In fact, in addition to the text input field, there will also be a Label that displays the Caption header, and these two controls will be placed in the Panel, which will actually return as the result of the Create method. Based on this example, inheriting from UserInputControl we will create all other user controls.
    Plus, the input form should wait for the end of user input, which I implemented by launching the main installer control flow in a separate stream with the continuation of the click on the Next button.
    Serialization
    I chose the XML serialization format because it is well supported by the NET platform. I will use the System.Xml.Serialization.XmlSerializer class, because of its ease of use, serializing an object in it requires writing only three lines of code, while additional flexibility (if required) is achieved by using attributes from the System.Xml.Serialization namespace .
    Here is the code:
    // сериализация
    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);
    

    As we see, the following are the input arguments for it: the path to the file where the serialized object will be written; the object itself; and the type of serializable object. This code is great for serializing not inherited from other classes. But if we are going to serialize, for example, the CreateFile class, whose inheritance hierarchy is CreateFile <- TransactionalCommand <- Command, then at T = Command, the serializer will tell us about the need to use the XmlInclude attribute, which tells the serializer all possible nested and parent classes used in serializable object. And this would not be so problematic if these attributes did not need to be applied to the base class Command, specifying for it one attribute XmlInclude for all its possible descendants. And since I guess that users will increase the collection of commands in the form of plugins without access to the source code of the Command class, which means that the serializer will not be able to serialize all these classes added by users. Fortunately, there is a way out. All Included types can be set not only with attributes, but also with direct arguments of the XmlSerializer constructor, into which the second argument can be an array of Type [] included types.
    Type[] types;
    XmlSerializer ser = new XmlSerializer(typeof(T), types);
    

    And this means that when creating an instance of the serializer, we need to have information about all the types used. That is, some meta-information about the installer.
    Plugins and Metadata
    I will introduce the GinMetaData class for this
    public class GinMetaData
    {
        public List Commands { get; private set; }
        public Type[] IncludedTypes { get; private set; }
        public static GinMetaData GetInstance();
        public void Plugin(string folderPath)
        public ExternalCommand GetCommandByName(string name)
    }
    

    I suppose that in the application there will always be only one instance of this class, which means we will implement it as a singleton. In this case, the IncludedTypes property will contain all types that can be used by the serializer, and types from both the main installer library and all its plugins. To connect new plugins to the metadata, the Plugin method is used, which takes the path to the plugins folder as input. If you call it several for different folders, then all the NET-libraries will be connected to the application in the form of plug-ins (unless of course they contain new commands and auxiliary classes).
    Links to all the commands available to the installer are loaded into the GinMetaData.Commands list. To do this, I created the LoadCommandsFrom (Assembly) method in the GinMetaData class:
    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);
            }
        }
    }
    

    It remains only to consider the ExternalCommand class used in this code. Here is its interface:
    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()
    }
    

    Each instance of ExternalCommand is essentially a wrapper around an instance of each of the classes that are descendants of the Command class. The ExternalCommand class has one constructor with an argument of type Type - a type loaded from the NET assembly. Since plug-in assemblies in the general case can contain not only commands, but also any auxiliary classes, before trying to create an instance of Command from a type loaded from the assembly, you need to check whether the loaded type is a valid command. The static method ContainsCommand (Type) just checks the loaded type for compliance with all formal requirements - the type must be inherited from Command or from any of its descendants, the type must not be abstract, the type must have a default constructor (serializers do not work without it),
    The CommandType property stores the Type object of reflection of the loaded command, the Instance property stores the instance of the loaded command created using the Constructor constructor. The Properties property provides an array of command properties. Since the command instance is stored in ExternalCommand as a reference to Command, the parent class of all commands, the interface of which has only the Do () method and nothing else, which means that the properties of this instance are not available directly, so I exported all the properties directly as array PropertyInfo. But this property is mainly needed only for listing properties. To Set and read each specific property, the ExternalCommand class has two methods: GetProperty (string key), and SetProperty (string key, object value).
    The Metadata property is any metadata about the loaded command. I meant that this metadata will be used to display commands in the interface of the visual package designer. It is not the subject of this article, but it is necessary to imply its existence. In the metadata of the command, you can store such parameters of the command as its name, its description, the way it is displayed in the package designer, etc. At the moment, the team metadata contains only two parameters: the name of the team, its description and the name of the group of teams:
    public class CommandMetadata
    {
        public string Name { get; set; }
        public string Desription { get; set; }
        public string Group { get; set; }
    }
    

    A group of commands, this is a text string - the name of the grouping node of the command in the interface of the package designer. It is necessary for structuring a large number of commands in the list by groups, such as, for example: file operations, IIS management, SQL commands, structuring commands. And other groups.
    Metadata is attached to the team using attributes. So far, I only have one metadata attribute for the GinNameAttribute command, here is a description of it:
    [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; }
    }
    

    It is applied to the command class, for example like this:
    [GinName( Name = "ВыполнитьЕсли-Иначе", Description = "Условный оператор if-then-else", Group = "Управление пакетом")]
    public class ExecuteIf : TransactionalCommand, IContainerCommand
    {
      // ……
    }
    

    When each command is loaded into an ExternalCommand instance, its attributes are also read from the command, including the GinNameAttribute attribute, which is then converted to an instance of the CommandMetadata class.
    The source code of the installer, as well as three typical scenarios for its use (package creation, execution and rollback), I posted in the repository on google-code . You can use it for your own purposes. I think that this was the last post on the topic of designing the installer.

    Also popular now: