Статьи

Расширьте свои .NET-приложения с помощью дополнений

Вашему приложению не хватает классной функции Whizbang? Позвольте другим заинтересованным разработчикам расширить ваше приложение с помощью дополнений. Открыв свое приложение с дополнительными функциональными возможностями, вы можете создать процветающее сообщество вокруг своего приложения, потенциально даже рынка!

В сегодняшнем уроке мы научимся именно этому!


В зависимости от того, какую версию Visual Studio вы используете, а также версию платформы, на которую вы нацелены, некоторые снимки экрана могут немного отличаться.

Мы собираемся создать приложение для проверки концепции, которое при запуске загружает дополнения, найденные в одном из его подкаталогов. Эти дополнения являются сборками .NET, которые содержат по крайней мере один класс, который реализует определенный интерфейс, определенный нами. Концепции в этом руководстве должны быть легко перенесены в ваши существующие приложения без особого труда. Затем мы рассмотрим более полный пример использования компонентов пользовательского интерфейса, загруженных из надстройки.

Это руководство было написано с использованием Microsoft Visual Studio 2010 в VB.NET, ориентированном на .NET Framework 4.0. Основные концепции этого руководства должны работать на других языках CLR, начиная с .NET Framework 1.1 и выше. Есть небольшая функциональность, которая не будет работать в других языках CLR, но должна быть очень легко перенесена — и некоторые концепции, такие как обобщенные, которые, очевидно, не будут работать в .NET 1.1 и т. Д.

Примечание. Это руководство на уровне экспертов для людей, которым нравится конвертировать код между различными языками CLR и версиями .NET Framework. Это больше «замена двигателя автомобиля», чем «как управлять».


Примечание . Ваши ссылки могут отличаться в зависимости от выбранной версии .NET Framework.

Давайте начнем с реализации небольшой версии нашего приложения. Нам понадобятся три проекта в нашем решении:

  • ConsoleAppA : консольное приложение
  • ClassLibA : библиотека классов
  • AddonA : библиотека классов, которая будет действовать как наше дополнение

Добавьте ссылку из ConsoleAppA и AddonA в ClassLibA . Обозреватель решений должен выглядеть следующим образом:

Чтобы скомпилированный класс считался совместимым, в каждом дополнении для нашего приложения должен быть реализован определенный интерфейс. Этот интерфейс будет определять обязательные свойства и операции, которые должен иметь класс, чтобы наше приложение могло взаимодействовать с надстройкой без заминок. Мы могли бы также использовать abstract/MustInherit класс abstract/MustInherit в качестве основы для дополнений, но в этом примере мы будем использовать интерфейс.

Вот код для нашего интерфейса, который должен быть помещен в файл с именем IApplicationModule в библиотеке классов ClassLibA.

1
2
3
4
5
6
7
8
Public Interface IApplicationModule
 
    ReadOnly Property Id As Guid
    ReadOnly Property Name As String
 
    Sub Initialise()
 
End Interface

Свойства и методы, которые вы определяете в интерфейсе, являются произвольными. В этом случае я определил два свойства и один метод, но на практике вы можете и должны изменить их по мере необходимости.

Мы не собираемся использовать свойства Id или Name в нашем первом примере, но они полезные свойства для реализации, и вы, вероятно, захотите, чтобы они присутствовали, если вы используете надстройки в работе.

Теперь давайте создадим фактическое дополнение. Опять же, для того, чтобы любой класс считался надстройкой для нашего приложения, ему необходимо реализовать наш интерфейс — IApplicationModule .

Вот код нашей базовой надстройки, который должен быть помещен в файл с именем MyAddonClass в библиотеке классов AddonA .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
Imports ClassLibA
 
Public Class MyAddonClass
    Implements IApplicationModule
 
    Public ReadOnly Property Id As System.Guid Implements ClassLibA.IApplicationModule.Id
        Get
            Return New Guid(«adb86b53-2207-488e-b0f3-ecd13eae4042»)
        End Get
    End Property
 
    Public Sub Initialise() Implements ClassLibA.IApplicationModule.Initialise
        Console.WriteLine(«MyAddonClass is starting up …»)
        ‘Perform start-up initialisation here …
    End Sub
 
    Public ReadOnly Property Name As String Implements ClassLibA.IApplicationModule.Name
        Get
            Return «My first test add-on»
        End Get
    End Property
End Class

Далее нам нужен способ найти дополнения для нашего приложения. В этом примере мы будем предполагать, что папка Addons была создана в каталоге исполняемого файла. Если вы тестируете это в Visual Studio, имейте в виду каталог вывода проекта по умолчанию для проектов, а именно ./bin/debug/ , поэтому вам потребуется каталог ./bin/debug/Addons/ .

Поместите метод TryLoadAssemblyReference ниже в Module1 ConsoleAppA . Мы исследуем загруженную сборку с помощью Reflection . Псевдокодовое пошаговое описание его функциональности выглядит следующим образом:

  • Попробуйте загрузить указанный dllFilePath как сборку .NET
  • Если мы успешно загрузили сборку, продолжайте
  • Для каждого модуля в загруженной сборке
  • Для каждого типа в этом модуле
  • Для каждого интерфейса, реализованного этим типом
  • Если этот интерфейс является нашим дополнительным интерфейсом ( IApplicationModule ), then
  • Ведите учет этого типа. Завершить поиск.
  • Наконец, верните все найденные допустимые типы
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
Private Function TryLoadAssemblyReference(ByVal dllFilePath As String) As List(Of System.Type)
    Dim loadedAssembly As Assembly
    Dim listOfModules As New List(Of System.Type)
    Try
        loadedAssembly = Assembly.LoadFile(dllFilePath)
    Catch ex As Exception
    End Try
    If loadedAssembly IsNot Nothing Then
        For Each assemblyModule In loadedAssembly.GetModules
            For Each moduleType In assemblyModule.GetTypes()
                For Each interfaceImplemented In moduleType.GetInterfaces()
                    If interfaceImplemented.FullName = «ClassLibA.IApplicationModule» Then
                        listOfModules.Add(moduleType)
                    End If
                Next
            Next
        Next
    End If
    Return listOfModules
End Function

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

Первая часть — найти все файлы, которые могут быть надстройками, как показано ниже. Код выполняет некоторый относительно простой поиск файлов, и для каждого найденного DLL-файла пытается загрузить все допустимые типы из этой сборки. Библиотеки DLL, обнаруженные в папке Addons, не обязательно являются надстройками — они могут просто содержать дополнительную функциональность, необходимую для надстройки, но фактически не могут быть надстройкой как таковой.

01
02
03
04
05
06
07
08
09
10
11
Dim currentApplicationDirectory As String = My.Application.Info.DirectoryPath
Dim addonsRootDirectory As String = currentApplicationDirectory & «\Addons\»
Dim addonsLoaded As New List(Of System.Type)
 
If My.Computer.FileSystem.DirectoryExists(addonsRootDirectory) Then
    Dim dllFilesFound = My.Computer.FileSystem.GetFiles(addonsRootDirectory, Microsoft.VisualBasic.FileIO.SearchOption.SearchAllSubDirectories, «*.dll»)
    For Each dllFile In dllFilesFound
        Dim modulesFound = TryLoadAssemblyReference(dllFile)
        addonsLoaded.AddRange(modulesFound)
    Next
End If

Затем нам нужно сделать что-то для каждого найденного нами действительного типа. Приведенный ниже код создаст новый экземпляр типа, напечатает тип, вызовет метод Initialise() (это просто произвольный метод, который мы определили в нашем интерфейсе), а затем сохранит ссылку на этот экземплярный тип в списке уровня модуля. ,

1
2
3
4
5
6
7
8
If addonsLoaded.Count > 0 Then
    For Each addonToInstantiate In addonsLoaded
        Dim thisInstance = Activator.CreateInstance(addonToInstantiate)
        Dim thisTypedInstance = CType(thisInstance, ClassLibA.IApplicationModule)
        thisTypedInstance.Initialise()
        m_addonInstances.Add(thisInstance)
    Next
End If

Собрав все это вместе, наше консольное приложение должно начать выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Imports System.Reflection
 
Module Module1
 
    Private m_addonInstances As New List(Of ClassLibA.IApplicationModule)
 
    Sub Main()
        LoadAdditionalModules()
 
        Console.WriteLine(‘Finished loading modules …’)
        Console.ReadLine()
    End Sub
 
    Private Sub LoadAdditionalModules()
        Dim currentApplicationDirectory As String = My.Application.Info.DirectoryPath
        Dim addonsRootDirectory As String = currentApplicationDirectory & ‘\Addons\’
        Dim addonsLoaded As New List(Of System.Type)
 
        If My.Computer.FileSystem.DirectoryExists(addonsRootDirectory) Then
            Dim dllFilesFound = My.Computer.FileSystem.GetFiles(addonsRootDirectory, Microsoft.VisualBasic.FileIO.SearchOption.SearchAllSubDirectories, «*.dll»)
            For Each dllFile In dllFilesFound
                Dim modulesFound = TryLoadAssemblyReference(dllFile)
                addonsLoaded.AddRange(modulesFound)
            Next
        End If
 
        If addonsLoaded.Count > 0 Then
            For Each addonToInstantiate In addonsLoaded
                Dim thisInstance = Activator.CreateInstance(addonToInstantiate)
                Dim thisTypedInstance = CType(thisInstance, ClassLibA.IApplicationModule)
                thisTypedInstance.Initialise()
                m_addonInstances.Add(thisInstance)
            Next
        End If
    End Sub
 
    Private Function TryLoadAssemblyReference(ByVal dllFilePath As String) As List(Of System.Type)
        Dim loadedAssembly As Assembly
        Dim listOfModules As New List(Of System.Type)
        Try
            loadedAssembly = Assembly.LoadFile(dllFilePath)
        Catch ex As Exception
        End Try
        If loadedAssembly IsNot Nothing Then
            For Each assemblyModule In loadedAssembly.GetModules
                For Each moduleType In assemblyModule.GetTypes()
                    For Each interfaceImplemented In moduleType.GetInterfaces()
                        If interfaceImplemented.FullName = ‘ClassLibA.IApplicationModule’ Then
                            listOfModules.Add(moduleType)
                        End If
                    Next
                Next
            Next
        End If
        Return listOfModules
    End Function
End Module

На этом этапе мы сможем выполнить полную сборку нашего решения. Когда ваши файлы собраны, скопируйте вывод скомпилированной сборки проекта AddonA в папку ConsoleAppA . Папка Addons должна быть создана в папке /bin/debug/ . На моем компьютере с использованием Visual Studio 2010, расположения проекта по умолчанию и решения под названием «DotNetAddons» папка находится здесь:

C:\Users\jplenderleith\Documents\Visual Studio 2010\Projects\DotNetAddons\ConsoleAppA\bin\Debug\Addons

Мы должны увидеть следующий вывод при запуске кода:

1
2
MyAddonClass is starting up …
Finished loading modules …

В его нынешнем виде это не бросается в глаза или впечатляет, но демонстрирует ключевую функциональность, а именно то, что во время выполнения мы можем подобрать сборку и выполнить код в этой сборке, не имея каких-либо знаний об этой сборке. Это основа создания более сложных дополнений.


Далее мы рассмотрим создание нескольких надстроек для обеспечения дополнительных функций в приложении Windows Forms.

Подобно существующему решению, создайте новое решение со следующими проектами:

  • WinFormsAppA : приложение Windows Forms
  • ClassLibA : библиотека классов
  • UIAddonA : библиотека классов, в которой будет размещено несколько дополнений с компонентами пользовательского интерфейса.

Подобно нашему предыдущему решению, проекты WinFormsAppA и UIAddonA должны иметь ссылки на ClassLibA . UIAddonA также понадобится ссылка на System.Windows.Forms для доступа к функциональности.

Мы сделаем быстрый и простой интерфейс для нашего приложения, состоящий из MenuStrip и DataGridView . MenuStrip управления MenuStrip должен иметь три элемента MenuItem

  • файл
  • Дополнения
  • Помогите

Прикрепите DataGridView к его родительскому контейнеру — самой форме. Мы соберем некоторый код для отображения ложных данных в нашей сетке. Надстройки MenuItem будут заполняться любыми надстройками, которые были загружены при запуске приложения.

Вот скриншот приложения на данный момент:

Давайте напишем некоторый код, чтобы дать основной форме WinFormsAppA некоторую функциональность. Когда форма загружается, она вызывает метод LoadAddons() который в настоящее время пуст — мы заполним это позже. Затем мы генерируем некоторые данные о сотрудниках, к которым привязываем наш DataGridView .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Public Class Form1
 
    Private m_testDataSource As DataSet
    Private m_graphicalAddons As List(Of System.Type)
 
    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        LoadAddons()
        m_testDataSource = GenerateTestDataSet()
        With DataGridView1
            .AutoGenerateColumns = True
            .AutoResizeColumns()
            .DataSource = m_testDataSource.Tables(0)
        End With
    End Sub
 
    Private Sub LoadAddons()
 
    End Sub
 
    Private Function GenerateTestDataSet() As DataSet
        Dim newDataSet As New DataSet
        Dim newDataTable As New DataTable
        Dim firstNames = {«Mary», «John», «Paul», «Justyna», «Michelle», «Andrew», «Michael»}
        Dim lastNames = {«O’Reilly», «Murphy», «Simons», «Kelly», «Gates», «Power»}
        Dim deptNames = {«Marketing», «Sales», «Technical», «Secretarial», «Security»}
 
        With newDataTable
            .Columns.Add(«EmployeeId», GetType(Integer))
            .Columns.Add(«Name», GetType(String))
            .Columns.Add(«IsManager», GetType(Boolean))
            .Columns.Add(«Department», GetType(String))
        End With
 
        For i = 1 To 100
            Dim newDataRow As DataRow = newDataTable.NewRow()
            With newDataRow
                .Item(«EmployeeId») = i
                .Item(«Name») = firstNames(i Mod firstNames.Count) & » » & lastNames(i Mod lastNames.Count)
                .Item(«IsManager») = ((i Mod 20) = 0)
                .Item(«Department») = deptNames(i Mod deptNames.Count)
            End With
            newDataTable.Rows.Add(newDataRow)
        Next
 
        newDataSet.Tables.Add(newDataTable)
        Return newDataSet
    End Function
 
End Class

Когда мы запускаем приложение, оно должно выглядеть так:

Мы вернемся к нашему пустому LoadAddons() после того, как определим интерфейсы дополнений.

Давайте определим наш дополнительный интерфейс. Компоненты пользовательского интерфейса будут перечислены в MenuStrip надстроек MenuStrip и откроют форму окна, которая имеет доступ к данным, с которыми связана наша DataGrid . В проект ClassLibA добавьте новый файл интерфейса с именем IGraphicalAddon и вставьте в него следующий код:

1
2
3
4
5
6
7
Public Interface IGraphicalAddon
 
    ReadOnly Property Name As String
    WriteOnly Property DataSource As DataSet
    Sub OnClick(ByVal sender As Object, ByVal e As System.EventArgs)
 
End Interface
  • У нас есть свойство Name , которое самоочевидно.
  • Источник данных должным образом используется для «подачи» данных в эту надстройку.
  • Метод OnClick , который будет использоваться в качестве обработчика, когда пользователи нажимают на запись нашего дополнения в меню дополнений.

Добавьте библиотеку классов AddonLoader в ClassLibA сборки ClassLibA со следующим кодом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Imports System.Reflection
 
Public Class AddonLoader
    Public Enum AddonType
        IGraphicalAddon = 10
        ISomeOtherAddonType2 = 20
        ISomeOtherAddonType3 = 30
    End Enum
 
    Public Function GetAddonsByType(ByVal addonType As AddonType) As List(Of System.Type)
        Dim currentApplicationDirectory As String = My.Application.Info.DirectoryPath
        Dim addonsRootDirectory As String = currentApplicationDirectory & «\Addons\»
        Dim loadedAssembly As Assembly
        Dim listOfModules As New List(Of System.Type)
 
        If My.Computer.FileSystem.DirectoryExists(addonsRootDirectory) Then
            Dim dllFilesFound = My.Computer.FileSystem.GetFiles(addonsRootDirectory, Microsoft.VisualBasic.FileIO.SearchOption.SearchAllSubDirectories, «*.dll»)
            For Each dllFile In dllFilesFound
 
                Try
                    loadedAssembly = Assembly.LoadFile(dllFile)
                Catch ex As Exception
                End Try
                If loadedAssembly IsNot Nothing Then
                    For Each assemblyModule In loadedAssembly.GetModules
                        For Each moduleType In assemblyModule.GetTypes()
                            For Each interfaceImplemented In moduleType.GetInterfaces()
                                If interfaceImplemented.Name = addonType.ToString Then
                                    listOfModules.Add(moduleType)
                                End If
                            Next
                        Next
                    Next
                End If
 
            Next
        End If
 
        Return listOfModules
    End Function
End Class

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

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

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

Сказав это, тем не менее, может быть желательно добавить ссылку на все надстройки в какой-либо строке меню, щелкните по которой будет отображаться форма параметров надстройки. Это, однако, выходит за рамки данного руководства.

Давайте создадим фактическое дополнение для пользовательского интерфейса. В сборочный проект UIAddonA добавьте следующие два файла; класс с именем UIReportAddon1 и UIReportAddon1 форма с именем UIReportAddon1Form . Класс UIReportAddon1 будет содержать следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Imports ClassLibA
 
Public Class UIReportAddon1
    Implements ClassLibA.IGraphicalAddon
 
    Private _dataSource As DataSet
    Public WriteOnly Property DataSource As System.Data.DataSet Implements IGraphicalAddon.DataSource
        Set(ByVal value As System.Data.DataSet)
            _dataSource = value
        End Set
    End Property
 
    Public ReadOnly Property Name As String Implements IGraphicalAddon.Name
        Get
            Return «Managers Report»
        End Get
    End Property
 
    Public Sub OnClick(ByVal sender As Object, ByVal e As System.EventArgs) Implements IGraphicalAddon.OnClick
        Dim newUiReportingAddonForm As New UIReportAddon1Form
        newUiReportingAddonForm.SetData(_dataSource)
        newUiReportingAddonForm.ShowDialog()
    End Sub
End Class

В UIReportAddon1Form добавьте DataGridView который UIReportAddon1Form в его родительском контейнере. Затем добавьте следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
Public Class UIReportAddon1Form
 
    Private m_providedDataSource As DataSet
 
    Public Sub SetData(ByVal DataSource As System.Data.DataSet)
        m_providedDataSource = DataSource
        FilterAndShowData()
    End Sub
 
    Private Sub FilterAndShowData()
        Dim managers = m_providedDataSource.Tables(0).Select(«IsManager = True»)
        Dim newDataTable = m_providedDataSource.Tables(0).Clone
        For Each dr In managers
            newDataTable.ImportRow(dr)
        Next
        DataGridView1.DataSource = newDataTable
    End Sub
 
    Private Sub UIReportAddon1Form_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Text = «List of managers»
    End Sub
End Class

Мы просто предоставляем средства для отправки данных в форму, а затем позволяем ей решать, что делать с этими данными. В этом случае мы собираемся выполнить Select() для DataTable , получив обратно список всех сотрудников, которые являются менеджерами.

Теперь пришло время завершить наш незаконченный LoadAddons() в нашем проекте WinFormsAppA . Этот код будет инструктировать метод ClassLibA.AddonLoader для поиска определенного типа надстройки — в данном случае надстройки IGraphicalAddon .

Для каждой найденной надстройки выполняются следующие действия

  • Создайте новый экземпляр дополнения
  • Введите экземпляр дополнения в IGraphicalAddon
  • Заполните его свойство DataSource
  • Добавить дополнение в пункт меню дополнений
  • Добавить обработчик для события Click этого пункта меню
01
02
03
04
05
06
07
08
09
10
11
12
Private Sub LoadAddons()
    Dim loader As New ClassLibA.AddonLoader
    Dim addonsLoaded = loader.GetAddonsByType(ClassLibA.AddonLoader.AddonType.IGraphicalAddon)
 
    For Each graphicalAddon In addonsLoaded
        Dim thisInstance = Activator.CreateInstance(graphicalAddon)
        Dim typedAddon = CType(thisInstance, ClassLibA.IGraphicalAddon)
        typedAddon.DataSource = m_testDataSource
        Dim newlyAddedToolstripItem = AddonsToolStripMenuItem.DropDownItems.Add(typedAddon.Name)
        AddHandler newlyAddedToolstripItem.Click, AddressOf typedAddon.OnClick
    Next
End Sub

Убедитесь, что вы можете выполнить полную успешную компиляцию решения. Скопируйте UIAddonA.dll из выходной папки UIAddonA проекта UIAddonA в папку /bin/debug/Addons/ проекта WinFormsAppA . Когда вы запустите проект, вы должны увидеть сетку данных, как и раньше. Если вы щелкнете в меню дополнений, вы должны увидеть ссылку на только что созданное дополнение.

Когда мы OnClick пункту меню «Отчет менеджера», это вызывает вызов метода для метода OnClick реализованного в нашей надстройке, что приведет к появлению UIReportAddon1Form , как UIReportAddon1Form ниже.


Мы видели два примера создания приложения, которое может при необходимости подбирать и запускать надстройки. Как я упоминал ранее, интегрировать этот тип функциональности в существующие приложения должно быть достаточно просто. Вопрос о том, какой уровень контроля вы должны (или хотите) предоставить сторонним разработчикам, — это совсем другой вопрос.

Я надеюсь, что вы получили удовольствие от этого урока! Большое спасибо за чтение!