๐Plugins development
Plugin Development Guide
Introduction
This guide will walk you through the process of developing plugins for the Asv.Drones.Gui
project. We'll use Asv.Drones.Gui.Plugin.Weather
as an example as it's a simple project mainly geared towards educational purposes.
All the source code of the project being analyzed is available in the repository on GitHub. Take a closer look - Asv.Drones.Gui.Plugin.Weather.
Project Naming
Once you've decided on a project name, follow the plugin naming rule. The main application (Asv.Drones.Gui
) uses a composition container to load external libraries, which implies that your plugins should be implemented as libraries. Moreover, your library files should follow the naming format Asv.Drones.Gui.Plugin.**YourPluginName**
. In our case, it will be Asv.Drones.Gui.Plugin.Weather
. This naming convention is crucial for the composition container to recognize and incorporate your plugin during the program start.
Project Structure and Dependencies
The structure of your files and folders should mirror that of Asv.Drones.Gui. The final project structure is depicted below.
You can see that Asv.Drones.Gui
project is present in our plugin solution. You need to add it as a Git submodule into your solution root folder as displayed in the image below.
Next, manually add all the existing projects from Asv.Drones.Gui
.
Ensure the addition of crucial dependencies such as:
Resource files (RS.resx) - necessary for text localization.
App.axaml file - helps in importing and exporting styles and custom controls.
Asv.Drones.Gui.Custom.props file - used for managing the versions of required NuGet packages.
Below is the structure of Asv.Drones.Gui.Custom.props
file. Copy this structure as it mentions the versions of all critical NuGet packages.
And you must do the same thing into Weather project file.
Once we've completed the initial steps, we can proceed further.
Our next task involves creating a class that will act as the entry point for our plugin. If we refer to the code provided earlier, we can see that this class is named "WeatherPlugin.cs".
Let's delve into the details of this class!
The WeatherPlugin class implements the IPluginEntryPoint interface and the PluginEntryPoint attribute. This designates the WeatherPlugin class as a plugin entry point. The creation policy for the entry point is always shared.
The plugin project is organized into several folders:
Controls: Contains all custom controls.
Service: Provides all services.
Shell: Includes all shell pages, view-models, and views.
This organization mirrors the structure of the Asv.Drones.Gui solution files. Let's explore the contents of these folders:
Controls:
The only file here is WindIndicator. This simple custom control adjusts to the given wind angle.
Service:
This folder implements the weather service class and interface. The Providers folder currently houses two weather providers: Windy and OpenWeatherMap. The service is implemented here for the following reasons: to save and load the last weather data when the weather button is displayed, to download weather data from the selected provider, to save the last selected provider and its API key, and to control the visibility of the action button.
Shell:
There are two views and view-models for the weather. The first is used to add an action button to our flight page.
The second is used to display the weather settings in the program settings list.
Code Explanation
Weather Action Button
To better understand the action button's functionality, let's examine its view, code-behind, and view-model!
We'll start with the view:
Namespaces: The script starts by defining namespaces. These help the XAML parser understand the meaning of the elements and attributes in your markup. Apart from the standard XAML namespaces, additional namespaces for AvaloniaUI, Material Icons for Avalonia, and the specific Weather plugin are also included.
UserControl: This primary object represented by the top-level UserControl element is the primary object that this XAML defines. UserControl serves as a base class for creating custom, reusable controls.
The
x:Class
attribute specifies the code-behind class for this XAML file. Here, the value isAsv.Drones.Gui.Plugin.Weather.WeatherActionView
.The
x:DataType
attribute indicates the ViewModel that this View binds to, which in this case isAsv.Drones.Gui.Plugin.Weather.WeatherActionViewModel
.The
IsVisible
attribute binds to theVisibility
property ofWeatherActionViewModel
and determines the visibility of the UserControl.
Design DataContext: This attribute sets the design-time data context to an instance of
WeatherActionViewModel
. It's primarily used for design-time data binding in visual design tools.Button: This element establishes a button that triggers the
UpdateWeather
command from the ViewModel upon clicking. The button includes a StackPanel, which aligns several child elements horizontally.StackPanel: The
StackPanel
has itsOrientation
set toHorizontal
, aligning its child elements horizontally. It contains an Icon, two TextBlock elements presenting theTemperature
andWindSpeed
strings, and a customWindIndicator
control.Elements in StackPanel: The
MaterialIcon
,TextBlock
, andWindIndicator
elements inside theStackPanel
are databound to properties inWeatherActionViewModel
such as Temperature, WindDirection, and WindSpeed.
Next, let's examine the code-behind of the view:
Namespaces:
System.ComponentModel.Composition
is a namespace that comprises types used for creating extensible applications in the Managed Extensibility Framework (MEF).Asv.Drones.Gui.Core
,Avalonia.Markup.Xaml
, andAvalonia.ReactiveUI
are specific to the libraries utilized the application.
Annotations:
The
[ExportView(typeof(WeatherActionViewModel))]
attribute signifies that this class offers an exported view for the WeatherActionViewModel, which is used for dependency injection. It's a specific attribute of theAsv.Drones.Gui.Core
library.[PartCreationPolicy(CreationPolicy.NonShared)]
attribute, part of MEF, denotes that a new instance ofWeatherActionView
is initiated each time it's required.
Class Declaration:
The
WeatherActionView
class inherits fromReactiveUserControl<WeatherActionViewModel>
, a class from the ReactiveUI library that supports a reactive programming model.WeatherActionViewModel
is the view model that this view binds to.
Methods:
In the constructor (
public WeatherActionView()
), theInitializeComponent()
method is called. This method usesAvaloniaXamlLoader.Load(this)
to load the relevant Avalonia XAML for the current control.
Next, let's dive into the view-model:
Dependencies:
The class employs MEF (Managed Extensibility Framework) as a form of dependency injection, as indicated by the Export and ImportingConstructor attributes. The dependencies in this class include
IWeatherService
andILocalizationService
.
Design Mode Behavior:
The default constructor features a block of code that executes conditionally when the application operates in Design Mode, which is usually when it is being edited in a UI designer of an IDE. This block applies specific styling to the application and sets some default values.
ViewModel Properties:
The ViewModel exposes several properties:
UpdateWeather
is a command bound to a UI action (likely a button click event) that invokes theUpdateWeatherImpl
method.CurrentWeatherData
,WindSpeedString
,WindSpeed
,WindDirection
,Temperature
, andVisibility
are all Reactively bound properties. The ReactiveUI framework is used here for declarative UI updates. The[Reactive]
attribute instructs ReactiveUI to raise PropertyChanged events whenever the value of these properties changes.
Initialization:
The
Init
method establishes the command, the map center, and subscribes to theCurrentWeatherData
property's changes. Accordingly, it updates the UI (Wind direction, Wind speed, Temperature, and triggers theLastWeatherData
event) wheneverCurrentWeatherData
changes. Upon initializing it, the method returns an instance of the class.
Weather Settings
Here's the view:
The highest level describes a
UserControl
, which is a custom, reusable control composed of other controls.The namespaces declared at the top map XML namespaces to CLR (Common Language Runtime) namespaces. This mapping allows the XML markup to use types in the corresponding CLR namespaces.
The
x:Class
attribute specifies the CLR namespace and class name for the code-behind class of the XAML file.x:DataType
represents the type of object to be used for data binding in this view. Here, it represents a ViewModel type from the weather CLR namespace.The
Design.DataContext
element assigns a design-time DataContext, which is the ViewModel instance the IDE's designer uses to render the XAML at design-time.core:OptionsDisplayItem
is a custom control used in the application. Various properties, such asHeader
,Icon
,Description
,Expands
, andIsExpanded
, are set.Within the
OptionsDisplayItem
, aStackPanel
allows for the arrangement of multiplecore:OptionsDisplayItem
elements in a stack, either horizontally or vertically. A vertical arrangement is utilized here (which is the default).Inside each
core:OptionsDisplayItem
, there areToggleButton
,ComboBox
, andTextBox
controls used for different functionalities such as toggling visibility, selecting from multiple options, and entering text, respectively.{CompiledBinding}
is a markup extension that recommends a binding to be compiled for improved performance.{x:Static}
refers to a static property. For instance,{x:Static weather:RS.WeatherSettingsView_Header}
refers to the staticWeatherSettingsView_Header
property on theRS
class in theweather
namespace.The
ComboBox.ItemTemplate
property defines aDataTemplate
that describes how data objects should be displayed. In this case, theTextBlock
element is employed to display theName
property.
Next, let's explore the code-behind:
The
using
statements at the top import required references.The namespace
Asv.Drones.Gui.Plugin.Weather
contains this class. Namespaces help organize your code.The
[ExportView(typeof(WeatherSettingsViewModel))]
attribute marks this class for export via MEF (Managed Extensibility Framework). In this context, it's paired with a ViewModel typeWeatherSettingsViewModel
through theExportView
attribute. The framework will then inject instances of this view when necessary within your application.The
[PartCreationPolicy(CreationPolicy.NonShared)]
attribute indicates that new instances of this class should be created every time the class needs to be injected or consumed within the program. TheCreationPolicy.NonShared
specifies that MEF will not cache and reuse the instances of this part. Instead, it will always create a new instance ofWeatherSettingsView
whenever it is requested.ReactiveUserControl<WeatherSettingsViewModel>
is the base class thatWeatherSettingsView
inherits from. Avalonia.ReactiveUI leverages a specific UI programming paradigm inspired by functional reactive programming. In this situation,WeatherSettingsViewModel
is the associated ViewModel type.The
InitializeComponent
method is where the Avalonia UI gets loaded via theAvaloniaXamlLoader.Load(this)
line. The layout, controls, and styles for this component are expected to be defined in the corresponding XAML file.
Lastly, let's consider the shell's ViewModel:
The WeatherSettingsViewModel
class extends from SettingsPartBase
and implements the ISettingsPart
interface, serving as a ViewModel in an MVVM (Model-View-ViewModel) pattern. It's tasked with defining and managing the state and the operations related to the application's weather settings.
Here are several key points:
MEF and Dependency Injection: The class employs the Managed Extensibility Framework (MEF), evident from the
Export
andImportingConstructor
attributes, as a form of Dependency Injection.IWeatherService: This interface delivers methods and properties related to the app's weather functionality. The
WeatherSettingsViewModel
interacts with this service to manipulate and present data.ReactiveUI and Reactive Properties: The class uses ReactiveUI, an MVVM framework, for reactive programming to streamline state management. The
[Reactive]
attribute is applied to define properties (Visibility
,VisibilityButton
,CurrentWeatherProvider
, andCurrentWeatherProviderApiKey
) that will automatically notify the UI of any changes.Subscriptions: They enable the ViewModel to subscribe to changes in certain properties and execute corresponding actions. For example, one subscription responds to changes in the
Visibility
property and adjusts the text ofVisibilityButton
accordingly.Properties:
WeatherProviders
returns available weather providers, andWeatherIcon
,WeatherApiKeyIcon
, andWeatherSwitchIcon
methods fetch data to display specific icons in the UI.Overall, this class acts as a bridge between the view and the model, manipulating data given by
IWeatherService
and presenting it to the UI in an engaging manner. Changes in the reactive properties will reflect in the UI, offering real-time interactivity.
Weather service class and interface
Interface
Class
The WeatherService
class employs the MEF (Managed Extensibility Framework) for plugin management, as indicated by the Export
and ImportingConstructor
attributes. The MEF facilitates loose coupling between the main application and its extensions or plugins.
The WeatherServiceConfig
class outlines the configuration for the WeatherService
, such as the visibility setting, the current weather provider, the last weather data, and a dictionary for storing API keys of various weather providers.
Within the WeatherService
class:
The class inherits
ServiceWithConfigBase<WeatherServiceConfig>
, using a configuration of theWeatherServiceConfig
type.It contains an instance of
IEnumerable<IWeatherProviderBase>
, representing multiple weather data providers.Its constructor imports the configuration and the list of weather providers. The configuration is used to access and adjust properties like visibility, current weather provider, API key, and the last weather data.
The
IRxEditableValue<T>
instances denote reactive properties. Any change in these properties triggers a defined action. For instance, a modification in theVisibility
property triggers theSetVisibility
method.Every time there's a shift in a reactive property, the corresponding method (like
SetVisibility
,SetCurrentProvider
,SetCurrentProviderApiKey
,SetLastWeatherData
) is invoked, which in turn updates the configuration accordingly.The
GetWeatherData(GeoPoint location)
method fetches weather data for a specific location.
WeatherData and IWeatherProviderBase
The class at the top, WeatherData
, includes three properties: WindSpeed
, WindDirection
, and Temperature
. Each of these properties is of the double
type. Presumably, this class is intended to store weather-related data.
The IWeatherProviderBase
interface is declared at the bottom. It outlines a property ApiKey
, a read-only property Name
, and two GetWeatherData
methods. The ApiKey
is presumably used for authenticating with the weather data provider's API, while Name
likely represents the name of the weather provider.
The first GetWeatherData
method takes a GeoPoint
object as a parameter, whereas the second GetWeatherData
method takes two double
parameters for latitude and longitude. Both methods return a Task<WeatherData>
, suggesting these are asynchronous methods that fetch WeatherData
from a source, and are expected to be used with the async/await
structure.
One note to make is that the GeoPoint
type is not defined in this code snippet; it appears to be part of the Asv.Common
namespace.
Any class intending to provide weather data would implement this interface, permitting different providers to be easily swapped out without the rest of the codebase needing to know where the data comes from. By using an interface, concrete implementations can have distinct behaviors yet still conform to a contract defined by the interface, thus promoting code reusability and modularity.
Another example
Asv.Drones.Gui.Plugin.FlightDocs stands as another example of an open-source plugin implementation for the Asv.Drones.Gui project.
This illustrative plugin serves as a showcase, offering insights into the intricate art of extending the capabilities of the parent application. Its primary aim is to familiarize users with the fundamental intricacies of crafting their very own plugins and the vast potential they hold.
In this instructive endeavor, you will discover how to empower your interface with action buttons under the "Actions" banner on the map page, create custom widgets and even design custom control elements that align with your unique vision. Furthermore, it delves into the key elements of constructing services and the pivotal role providers play in these service-oriented extensions.
The project's file structure, meticulously crafted to emulate that of the primary Asv.Drones.Gui
project, is not a mere coincidence. Rather, it is a deliberate choice, as this structured approach proves most advantageous for the development and upkeep of open-source plugins meant to enrich the functionality of the core application.
How to build
Make sure the next components are installed:
.NET SDK 7 - https://dotnet.microsoft.com/en-us/download/dotnet/7.0
AvaloniaUI Templates - https://docs.avaloniaui.net/docs/get-started/install
Avalonia XAML development - https://docs.avaloniaui.net/docs/get-started/set-up-an-editor
After you installed all of these, you need to follow the steps:
Open terminal and clone this repository using
git clone git@github.com:asv-soft/asv-drones-gui-flight-docs.git
command (URL may be different);Open the cloned repository folder using
cd asv-drones-gui-flight-docs
;Execute
git submodule init
command to initialize Asv.Drones.Gui as a submodule;Execute
git submodule update
to set latest version on Asv.Drones.Gui submodule;Then you need to restore NuGet packages in a plugin project with
dotnet restore
,nuget restore
or through IDE;Finally - try to build your project with
dotnet build
or through IDE.
How to use
After building the source code of the plugin project, the final library should be placed in the directory of the already built Asv.Drones.Gui
application, the next time you launch the application CompositionContainer will see the library and add it to the common list of libraries loaded at startup.
Plugin development guide
Introduction
This guide will walk you through the process of developing plugins for the Asv.Drones.Gui
project. We'll use Asv.Drones.Gui.Plugin.FlightDocs
as an example as it's a simple project mainly geared towards educational purposes.
Project Naming
Once you've decided on a project name, follow the plugin naming rule. The main application (Asv.Drones.Gui
) uses a composition container to load external libraries, which implies that your plugins should be implemented as libraries. Moreover, your library files should follow the naming format Asv.Drones.Gui.Plugin.**YourPluginName**
. In our case, it will be Asv.Drones.Gui.Plugin.FlightDocs
. This naming convention is crucial for the composition container to recognize and incorporate your plugin during the program start.
Project Structure and Dependencies
The structure of your files and folders should mirror that of Asv.Drones.Gui. The final project structure is depicted below.
You can see that Asv.Drones.Gui
project is present in our plugin solution. You need to add it as a Git submodule into your solution root folder as displayed in the image below.
Next, manually add all the existing projects from Asv.Drones.Gui
.
Ensure the addition of crucial dependencies such as:
Resource files (RS.resx) - necessary for text localization.
App.axaml file - helps in importing and exporting styles and custom controls.
Asv.Drones.Gui.Custom.props file - used for managing the versions of required NuGet packages.
Below is the structure of Asv.Drones.Gui.Custom.props
file. Copy this structure as it mentions the versions of all critical NuGet packages.
And you must do the same thing into FlightDocs project file.
Once we've completed the initial steps, we can proceed further.
Our next task involves creating a class that will act as the entry point for our plugin. If we refer to the code provided earlier, we can see that this class is named "FlightDocsPlugin.cs".
Let's delve into the details of this class!
The FlightDocsPlugin class implements the IPluginEntryPoint interface and the PluginEntryPoint attribute. This designates the FlightDocsPlugin class as a plugin entry point. The creation policy for the entry point is always shared.
The plugin project has Map folder, which contains several more folders:
Actions: Contains all actions that can be perfomed within plugin.
Anchors: Contains all anchors used to mark points on a map.
Widgets: Includes all widgets and dialogs used by plugin.
This organization mirrors the structure of the Asv.Drones.Gui solution files. Let's explore the contents of these folders:
Actions:
This folder contains classes that implement several actions that can be perfomed while using this plugin. FlightZoneAction allows user to add points that represent the flight zone, TakeOffLandAction allows user to specify points where take off and landing will occure, AnchorMoverAction allows user to enable anchor editing mode where he can move anchors on map with drag-and-drop and MapZoomAction allows user to change map zoom.
Anchors:
This folder contains classes that implement anchors used in this plugin. FlightZoneAnchor is used to display flight zone points, FlightZonePolygon is used to draw a poligon that connects all flight zone points and TakeOffLandAnchor is used to display take off and land points.
Widgets:
This folder contains classes and views that implement widgets used in this plugin. FlightZoneMapWidget is displayed in the right side of the main screen and is used to change flight zone points location or delete them. TakeOffLandMapWidget is used to change take off and land points location or delete them. FlightPlanGeneratorMapWidget is used to fill in other flight zone relevant data, such as altitude of flight, date of flight and other information. It is later used to form Flight Plan data that can be used to request flight permission from authorities. FlightPlanView is a dialog popup that displays generated Flight Plan data. There is also a FlightZoneWidgetProvider class, which we will discuss in the next section of this guide.
Code Explanation
Basic Code Structure
The basics of code structure are the same as for any other MVVM application, written using Avalonia UI. You can check out more details about this topic in our Weather Plugin example written above.
Interaction between plugin and main software
Now lets discuss how plugins can interact with main projects codebase. For example, FlightZoneWidgetProvider class is used to create and provide map widget view models for flight zones. It takes in a localization service and configuration as inputs when constructed.
The main purpose of this class is to create view model instances for different map widgets related to flight zones and add them to the Source collection. It does this by calling the AddOrUpdate method in the constructor, passing new instances of the FlightZoneMapWidgetViewModel, TakeOffLandMapWidgetViewModel, and FlightPlanGeneratorMapWidgetViewModel classes. These view model classes are specific to different flight zone related map widgets. The FlightZoneWidgetProvider doesn't contain the implementation logic for these view models. It just handles creating them and adding them to the Source collection. The Source collection property is from the base ViewModelProviderBase class, which is a Core class from parent project asv-drones. This contains the view model instances that this provider creates. Other code can then get the appropriate view model from this Source collection for a given map widget type.
So in summary, the FlightZoneWidgetProvider class is responsible for creating and providing the view models for flight zone related map widgets. It encapsulates the view model creation logic in one place and exposes the view models through the Source collection for other code to use. This allows separating the view model creation from the consumption.
Another good example is FlightZoneMapAnchorProvider class, because it can update it's items dynamically. It is used to provide map anchor data to a flight zone map view model.
It takes in a SourceList of IMapAnchor objects as input via the Update method. The IMapAnchor objects contain the data for the anchors to display on the map (e.g. location coordinates, title, etc.). The Update method subscribes to events on the input SourceList to keep the FlightZoneMapAnchorProvider's own Source property in sync. When anchors are added or removed from the input, it adds or removes them from its own Source respectively. The Source property is used as the output - it contains the latest set of IMapAnchor objects that should be displayed on the map. By syncing it to the input SourceList, it ensures the map view model always has the updated anchor data.
The main logic flow is:
1. The Update method is called with a SourceList containing map anchors
2. It subscribes to events on that SourceList
3. When anchors are added/removed from the input, it updates its own Source property accordingly
4. The Source property is used by the map view model to show the anchors
So in summary, the FlightZoneMapAnchorProvider acts as a bridge between an input SourceList of anchors and the view model. It propagates changes to the anchors to keep the view model up to date. This allows the view model to always display the latest anchor data.
Last updated