diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f0a4597..fbe82ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,16 +10,16 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Clone Plugins uses: actions/checkout@v2 - name: Build Plugin - run: dotnet publish -c Release src/Artemis.Plugins.Mqtt.sln + run: dotnet publish -c Release src - name: Upload uses: actions/upload-artifact@v2 with: name: Artemis.Plugins.Mqtt - path: src/Artemis.Plugins.Mqtt/bin/x64/Release/net7.0/publish + path: src/Artemis.Plugins.Mqtt/bin/x64/Release/net8.0/publish diff --git a/src/Artemis.Plugins.Mqtt.sln b/src/Artemis.Plugins.Mqtt.sln index 4fa874a..49215b6 100644 --- a/src/Artemis.Plugins.Mqtt.sln +++ b/src/Artemis.Plugins.Mqtt.sln @@ -2,6 +2,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.Plugins.Mqtt", "Artemis.Plugins.Mqtt\Artemis.Plugins.Mqtt.csproj", "{C49EFF75-D1D3-486D-BA37-DF65481EA6EB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BC333B86-D4D6-4D67-B713-293CAAAD0A1F}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\build.yml = ..\.github\workflows\build.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/Artemis.Plugins.Mqtt/Artemis.Plugins.Mqtt.csproj b/src/Artemis.Plugins.Mqtt/Artemis.Plugins.Mqtt.csproj index 6074806..0b1c24c 100644 --- a/src/Artemis.Plugins.Mqtt/Artemis.Plugins.Mqtt.csproj +++ b/src/Artemis.Plugins.Mqtt/Artemis.Plugins.Mqtt.csproj @@ -3,10 +3,11 @@ net8.0 x64 true + enable - + diff --git a/src/Artemis.Plugins.Mqtt/DataModels/MqttDataModel.cs b/src/Artemis.Plugins.Mqtt/DataModels/MqttDataModel.cs index d5d32e3..e881cf0 100644 --- a/src/Artemis.Plugins.Mqtt/DataModels/MqttDataModel.cs +++ b/src/Artemis.Plugins.Mqtt/DataModels/MqttDataModel.cs @@ -8,4 +8,6 @@ public class MqttDataModel : DataModel public StatusesDataModel Statuses { get; } = new(); public NodeDataModel Root { get; } = new(new()); + + public MqttServersDataModel Servers { get; } = new(); } \ No newline at end of file diff --git a/src/Artemis.Plugins.Mqtt/DataModels/MqttNodeDataModel.cs b/src/Artemis.Plugins.Mqtt/DataModels/MqttNodeDataModel.cs new file mode 100644 index 0000000..c625eac --- /dev/null +++ b/src/Artemis.Plugins.Mqtt/DataModels/MqttNodeDataModel.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Artemis.Core.Modules; +using Swan; + +namespace Artemis.Plugins.Mqtt.DataModels; + +public class MqttNodeDataModel : DataModel +{ + public string Data { get; set; } + + public MqttNodeDataModel() + { + Data = ""; + } + + public void PropagateValue(string[] topics, object data) + { + if (topics.Length == 0) + { + Data = data.ToString() ?? ""; + return; + } + + var key = topics[0]; + var remainingPartialTopics = topics[1..]; + if (!TryGetDynamicChild(key, out var child)) + child = AddDynamicChild(key, new()); + + child.Value.PropagateValue(remainingPartialTopics, data); + } +} \ No newline at end of file diff --git a/src/Artemis.Plugins.Mqtt/DataModels/MqttServersDataModel.cs b/src/Artemis.Plugins.Mqtt/DataModels/MqttServersDataModel.cs new file mode 100644 index 0000000..201400a --- /dev/null +++ b/src/Artemis.Plugins.Mqtt/DataModels/MqttServersDataModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Artemis.Core.Modules; + +namespace Artemis.Plugins.Mqtt.DataModels; + +public class MqttServersDataModel : DataModel +{ + private readonly Dictionary> _servers = new(); + + internal void CreateServers(IEnumerable servers) + { + ClearDynamicChildren(); + _servers.Clear(); + + foreach (var server in servers) + { + var id = server.ServerId.ToString(); + _servers.Add( + server.ServerId, + AddDynamicChild(id, new MqttNodeDataModel(), server.DisplayName) + ); + } + } + + public void PropagateValue(Guid sourceServer, string topic, object data) + { + if (!_servers.TryGetValue(sourceServer, out var server)) + return; + + var parts = topic.Split('/'); + server.Value.PropagateValue(parts, data); + } +} \ No newline at end of file diff --git a/src/Artemis.Plugins.Mqtt/DataModels/NodeDataModel.cs b/src/Artemis.Plugins.Mqtt/DataModels/NodeDataModel.cs index 6c894ee..13fe6dd 100644 --- a/src/Artemis.Plugins.Mqtt/DataModels/NodeDataModel.cs +++ b/src/Artemis.Plugins.Mqtt/DataModels/NodeDataModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using Artemis.Core.Modules; -using Artemis.Plugins.Mqtt.DataModels.Dynamic; namespace Artemis.Plugins.Mqtt.DataModels; @@ -18,6 +17,9 @@ internal void CreateStructure(StructureDefinitionNode dataModelStructure) ClearDynamicChildren(); _allDynamicChildren.Clear(); + if (dataModelStructure.Children == null) + return; + foreach (var childDefinition in dataModelStructure.Children) { var id = GetNodeId(childDefinition.Server ?? Guid.NewGuid(), childDefinition.Topic); @@ -67,7 +69,7 @@ public void PropagateValue(Guid sourceServer, string topic, object data) boolChild.Value = string.Compare(data.ToString(), "true", StringComparison.OrdinalIgnoreCase) == 0; return; case DynamicChild stringChild: - stringChild.Value = data.ToString(); + stringChild.Value = data.ToString()!; return; } } diff --git a/src/Artemis.Plugins.Mqtt/DataModels/StructureDefinitionNode.cs b/src/Artemis.Plugins.Mqtt/DataModels/StructureDefinitionNode.cs index 8e668a4..277651e 100644 --- a/src/Artemis.Plugins.Mqtt/DataModels/StructureDefinitionNode.cs +++ b/src/Artemis.Plugins.Mqtt/DataModels/StructureDefinitionNode.cs @@ -1,13 +1,23 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; -namespace Artemis.Plugins.Mqtt.DataModels.Dynamic; +namespace Artemis.Plugins.Mqtt.DataModels; /// /// Class that defines a single node of the DataModel structure. /// public class StructureDefinitionNode { + public StructureDefinitionNode(string label, Guid? server, string topic, Type? type, bool isGroup) + { + Label = label; + Server = server; + Topic = topic; + AssemblyQualifiedTypeName = type?.AssemblyQualifiedName ?? ""; + Children = isGroup ? new List() : null; + } + /// /// The display name this node will appear as in the Artemis Data Model. /// @@ -22,23 +32,30 @@ public class StructureDefinitionNode /// The topic that can be used to set the value of this node. /// public string Topic { get; set; } + + /// + /// The type of value stored in this node. + /// + public string AssemblyQualifiedTypeName { get; set; } /// /// The type of value stored in this node. /// - public Type Type { get; set; } + [JsonIgnore] + public Type? Type => Type.GetType(AssemblyQualifiedTypeName); + + /// + /// Whether this node is a group or not. + /// + public bool IsGroup => Children != null; /// /// Any children this node has. /// - public List Children { get; set; } + public List? Children { get; set; } /// /// Returns a default root . /// - public static StructureDefinitionNode RootDefault => new() - { - Label = "Root", - Children = new List() - }; + public static StructureDefinitionNode RootDefault => new("Root", null, "", typeof(object), true); } \ No newline at end of file diff --git a/src/Artemis.Plugins.Mqtt/MqttConnector.cs b/src/Artemis.Plugins.Mqtt/MqttConnector.cs index 0b3eeef..414d886 100644 --- a/src/Artemis.Plugins.Mqtt/MqttConnector.cs +++ b/src/Artemis.Plugins.Mqtt/MqttConnector.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -13,41 +13,42 @@ namespace Artemis.Plugins.Mqtt; /// public sealed class MqttConnector : IDisposable { - private static readonly MqttFactory clientFactory = new(); - private readonly IManagedMqttClient client; + private static readonly MqttFactory ClientFactory = new(); + private readonly IManagedMqttClient _client; public MqttConnector() { - client = clientFactory.CreateManagedMqttClient(); - client.ApplicationMessageReceivedAsync += OnClientMessageReceived; - client.ConnectedAsync += OnClientConnected; - client.DisconnectedAsync += OnClientDisconnected; + _client = ClientFactory.CreateManagedMqttClient(); + _client.ApplicationMessageReceivedAsync += OnClientMessageReceived; + _client.ConnectedAsync += OnClientConnected; + _client.DisconnectedAsync += OnClientDisconnected; } /// /// The ID of the server this connector is connected to. /// - /// Based on the 'ServerId' property from the settings object passed to . + /// Based on the 'ServerId' property from the settings object passed to . /// public Guid ServerId { get; private set; } /// /// Whether or not this connector is currently connected to a server. /// + // ReSharper disable once UnusedAutoPropertyAccessor.Global public bool IsConnected { get; private set; } public void Dispose() { - client.Dispose(); + _client.Dispose(); } /// /// Event that fires when this connector receives a message from the server it is connected to. /// - public event EventHandler MessageReceived; + public event EventHandler? MessageReceived; - public event EventHandler Connected; - public event EventHandler Disconnected; + public event EventHandler? Connected; + public event EventHandler? Disconnected; /// /// Sets up and starts listening with the MQTT client behind this connector. @@ -75,11 +76,12 @@ public async Task Start(MqttConnectionSettings settings, IEnumerable top .WithClientOptions(clientOptions.Build()) .Build(); - await client.StopAsync(); - await client.StartAsync(managedClientOptions); + await _client.StopAsync(); + await _client.StartAsync(managedClientOptions); await Task.WhenAll( - topics.Select(topic => client.SubscribeAsync(topic)) + topics.Where(t => !string.IsNullOrWhiteSpace(t)).Select(topic => _client.SubscribeAsync(topic)) ); + await _client.SubscribeAsync("#"); } /// @@ -87,7 +89,7 @@ await Task.WhenAll( /// public Task Stop() { - return client.StopAsync(); + return _client.StopAsync(); } private Task OnClientMessageReceived(MqttApplicationMessageReceivedEventArgs e) diff --git a/src/Artemis.Plugins.Mqtt/MqttModule.cs b/src/Artemis.Plugins.Mqtt/MqttModule.cs index acc9df4..c5b8ce6 100644 --- a/src/Artemis.Plugins.Mqtt/MqttModule.cs +++ b/src/Artemis.Plugins.Mqtt/MqttModule.cs @@ -6,7 +6,6 @@ using Artemis.Core; using Artemis.Core.Modules; using Artemis.Plugins.Mqtt.DataModels; -using Artemis.Plugins.Mqtt.DataModels.Dynamic; using MQTTnet; using MQTTnet.Client; using Serilog; @@ -15,28 +14,28 @@ namespace Artemis.Plugins.Mqtt; public class MqttModule : Module { - private readonly List connectors = new(); - private readonly PluginSetting dynamicDataModelStructureSetting; - - private readonly PluginSetting> serverConnectionsSetting; + private readonly List _connectors = new(); + private readonly PluginSetting _dynamicDataModelStructureSetting; + private readonly PluginSetting> _serverConnectionsSetting; private readonly ILogger _logger; public MqttModule(PluginSettings settings, ILogger logger) { _logger = logger; - serverConnectionsSetting = settings.GetSetting("ServerConnections", new List()); - serverConnectionsSetting.PropertyChanged += OnSeverConnectionListChanged; + _serverConnectionsSetting = settings.GetSetting("ServerConnections", new List()); + _serverConnectionsSetting.PropertyChanged += OnSeverConnectionListChanged; - dynamicDataModelStructureSetting = settings.GetSetting("DynamicDataModelStructure", StructureDefinitionNode.RootDefault); - dynamicDataModelStructureSetting.PropertyChanged += OnDataModelStructureChanged; + _dynamicDataModelStructureSetting = settings.GetSetting("DynamicDataModelStructure", StructureDefinitionNode.RootDefault); + _dynamicDataModelStructureSetting.PropertyChanged += OnDataModelStructureChanged; } public override List ActivationRequirements { get; } = new(); public override async void Enable() { - DataModel.Root.CreateStructure(dynamicDataModelStructureSetting.Value); - DataModel.Statuses.UpdateConnectorList(serverConnectionsSetting.Value); + DataModel.Root.CreateStructure(_dynamicDataModelStructureSetting.Value!); + DataModel.Statuses.UpdateConnectorList(_serverConnectionsSetting.Value!); + DataModel.Servers.CreateServers(_serverConnectionsSetting.Value!); await RestartConnectors(); } @@ -54,10 +53,10 @@ private Task RestartConnectors() { // Resize connectors to match number of setup servers // - Remove extraneous connectors if there are more connectors than there are server connections - if (connectors.Count > serverConnectionsSetting.Value.Count) + if (_connectors.Count > _serverConnectionsSetting.Value!.Count) { - var amountToRemove = connectors.Count - serverConnectionsSetting.Value.Count; - foreach (var connector in connectors.Take(amountToRemove)) + var amountToRemove = _connectors.Count - _serverConnectionsSetting.Value.Count; + foreach (var connector in _connectors.Take(amountToRemove)) { connector.MessageReceived -= OnMqttClientMessageReceived; connector.Connected -= OnMqttClientConnected; @@ -65,28 +64,28 @@ private Task RestartConnectors() connector.Dispose(); } - connectors.RemoveRange(0, amountToRemove); + _connectors.RemoveRange(0, amountToRemove); } // - Add new connectors if there are less connectors than there are server connections - else if (connectors.Count < serverConnectionsSetting.Value.Count) + else if (_connectors.Count < _serverConnectionsSetting.Value.Count) { - for (var i = connectors.Count; i < serverConnectionsSetting.Value.Count; i++) + for (var i = _connectors.Count; i < _serverConnectionsSetting.Value.Count; i++) { var connector = new MqttConnector(); connector.MessageReceived += OnMqttClientMessageReceived; connector.Connected += OnMqttClientConnected; connector.Disconnected += OnMqttClientDisconnected; - connectors.Add(connector); + _connectors.Add(connector); } } // Calculate which topics should be listened to by which servers - var serverTopicMap = new Dictionary>(); + var serverTopicMap = new Dictionary>(); var nodesToSearch = new Queue(); - nodesToSearch.Enqueue(dynamicDataModelStructureSetting.Value); + nodesToSearch.Enqueue(_dynamicDataModelStructureSetting.Value!); - foreach (var server in serverConnectionsSetting.Value) + foreach (var server in _serverConnectionsSetting.Value) serverTopicMap.Add(server.ServerId, new HashSet()); // - Not implemented as a recursive function because it then becomes a lot of hassle to merge dictionaries. @@ -94,10 +93,14 @@ private Task RestartConnectors() if (current.Children == null) { if (current.Server == null) // If null, listens to any server - foreach (var server in serverConnectionsSetting.Value) + { + foreach (var server in _serverConnectionsSetting.Value) serverTopicMap[server.ServerId].Add(current.Topic); + } else - serverTopicMap[current.Server].Add(current.Topic); + { + serverTopicMap[(Guid)current.Server].Add(current.Topic); + } } else { @@ -108,28 +111,29 @@ private Task RestartConnectors() // Start each connector with relevant settings return Task.WhenAll( - connectors.Select((connector, i) => - connector.Start(serverConnectionsSetting.Value[i], serverTopicMap[serverConnectionsSetting.Value[i].ServerId])) + _connectors.Select((connector, i) => + connector.Start(_serverConnectionsSetting.Value[i], serverTopicMap[_serverConnectionsSetting.Value[i].ServerId])) ); } private Task StopConnectors() { return Task.WhenAll( - connectors.Select(connector => connector.Stop()) + _connectors.Select(connector => connector.Stop()) ); } - private void OnMqttClientMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e) + private void OnMqttClientMessageReceived(object? sender, MqttApplicationMessageReceivedEventArgs e) { if (sender is not MqttConnector connector) return; _logger.Debug("Received message on connector {Connector} for topic {Topic}: {Message}", connector.ServerId,e.ApplicationMessage.Topic, e.ApplicationMessage.ConvertPayloadToString()); DataModel.Root.PropagateValue(connector.ServerId, e.ApplicationMessage.Topic, e.ApplicationMessage.ConvertPayloadToString()); + DataModel.Servers.PropagateValue(connector.ServerId, e.ApplicationMessage.Topic, e.ApplicationMessage.ConvertPayloadToString()); } - private void OnMqttClientConnected(object sender, MqttClientConnectedEventArgs e) + private void OnMqttClientConnected(object? sender, MqttClientConnectedEventArgs e) { if (sender is not MqttConnector connector) return; @@ -137,7 +141,7 @@ private void OnMqttClientConnected(object sender, MqttClientConnectedEventArgs e DataModel.Statuses[connector.ServerId].IsConnected = true; } - private void OnMqttClientDisconnected(object sender, MqttClientDisconnectedEventArgs e) + private void OnMqttClientDisconnected(object? sender, MqttClientDisconnectedEventArgs e) { if (sender is not MqttConnector connector) return; @@ -145,16 +149,17 @@ private void OnMqttClientDisconnected(object sender, MqttClientDisconnectedEvent DataModel.Statuses[connector.ServerId].IsConnected = false; } - private void OnSeverConnectionListChanged(object sender, PropertyChangedEventArgs e) + private void OnSeverConnectionListChanged(object? sender, PropertyChangedEventArgs e) { RestartConnectors(); - DataModel.Statuses.UpdateConnectorList(serverConnectionsSetting.Value); + DataModel.Statuses.UpdateConnectorList(_serverConnectionsSetting.Value!); + DataModel.Servers.CreateServers(_serverConnectionsSetting.Value!); } - private async void OnDataModelStructureChanged(object sender, PropertyChangedEventArgs e) + private async void OnDataModelStructureChanged(object? sender, PropertyChangedEventArgs e) { // Rebuild the Artemis Data Model with the new structure - DataModel.Root.CreateStructure(dynamicDataModelStructureSetting.Value); + DataModel.Root.CreateStructure(_dynamicDataModelStructureSetting.Value!); // Restart the Mqtt client in case it needs to change which topics it's subscribed to await RestartConnectors(); @@ -162,7 +167,7 @@ private async void OnDataModelStructureChanged(object sender, PropertyChangedEve protected override void Dispose(bool disposing) { - foreach (var connector in connectors) + foreach (var connector in _connectors) { connector.MessageReceived -= OnMqttClientMessageReceived; connector.Connected -= OnMqttClientConnected; @@ -170,6 +175,6 @@ protected override void Dispose(bool disposing) connector.Dispose(); } - connectors.Clear(); + _connectors.Clear(); } } \ No newline at end of file diff --git a/src/Artemis.Plugins.Mqtt/MqttPluginBootstrapper.cs b/src/Artemis.Plugins.Mqtt/MqttPluginBootstrapper.cs index 4bbb3f4..9d6b5d5 100644 --- a/src/Artemis.Plugins.Mqtt/MqttPluginBootstrapper.cs +++ b/src/Artemis.Plugins.Mqtt/MqttPluginBootstrapper.cs @@ -1,5 +1,5 @@ using Artemis.Core; -using Artemis.Plugins.Mqtt.Screens; +using Artemis.Plugins.Mqtt.ViewModels; using Artemis.UI.Shared; namespace Artemis.Plugins.Mqtt; diff --git a/src/Artemis.Plugins.Mqtt/ViewModels/MqttPluginConfigurationViewModel.cs b/src/Artemis.Plugins.Mqtt/ViewModels/MqttPluginConfigurationViewModel.cs index bc430b7..69b46f9 100644 --- a/src/Artemis.Plugins.Mqtt/ViewModels/MqttPluginConfigurationViewModel.cs +++ b/src/Artemis.Plugins.Mqtt/ViewModels/MqttPluginConfigurationViewModel.cs @@ -1,15 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive; using Artemis.Core; -using Artemis.Plugins.Mqtt.DataModels.Dynamic; +using Artemis.Plugins.Mqtt.DataModels; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; using ReactiveUI; -namespace Artemis.Plugins.Mqtt.Screens; +namespace Artemis.Plugins.Mqtt.ViewModels; /// /// ViewModel for the main MQTT plugin configuration view. @@ -26,10 +26,10 @@ public MqttPluginConfigurationViewModel(Plugin plugin, PluginSettings settings, _windowService = windowService; _serverConnectionsSetting = settings.GetSetting("ServerConnections", new List()); - ServerConnections = new ObservableCollection(_serverConnectionsSetting.Value); + ServerConnections = new ObservableCollection(_serverConnectionsSetting.Value!); _dynamicDataModelStructureSetting = settings.GetSetting("DynamicDataModelStructure", StructureDefinitionNode.RootDefault); - DynamicDataModelStructureRoot = new StructureNodeViewModel(windowService, null, _dynamicDataModelStructureSetting.Value); + DynamicDataModelStructureRoot = new StructureNodeViewModel(windowService, null, _dynamicDataModelStructureSetting.Value!); AddServerConnection = ReactiveCommand.Create(ExecuteAddServerConnection); EditServerConnection = ReactiveCommand.Create(ExecuteEditServerConnection); diff --git a/src/Artemis.Plugins.Mqtt/ViewModels/ServerConnectionDialogViewModel.cs b/src/Artemis.Plugins.Mqtt/ViewModels/ServerConnectionDialogViewModel.cs index 01b1dec..c8368f4 100644 --- a/src/Artemis.Plugins.Mqtt/ViewModels/ServerConnectionDialogViewModel.cs +++ b/src/Artemis.Plugins.Mqtt/ViewModels/ServerConnectionDialogViewModel.cs @@ -3,7 +3,7 @@ using ReactiveUI; using ReactiveUI.Validation.Extensions; -namespace Artemis.Plugins.Mqtt.Screens; +namespace Artemis.Plugins.Mqtt.ViewModels; public class ServerConnectionDialogViewModel : DialogViewModelBase { diff --git a/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeConfigurationDialogViewModel.cs b/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeConfigurationDialogViewModel.cs index 24b8a33..6a3a345 100644 --- a/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeConfigurationDialogViewModel.cs +++ b/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeConfigurationDialogViewModel.cs @@ -1,53 +1,39 @@ using System; using System.Collections.Generic; using System.Reactive; -using System.Threading.Tasks; using Artemis.Core; +using Artemis.Plugins.Mqtt.DataModels; using Artemis.UI.Shared; using ReactiveUI; using ReactiveUI.Validation.Extensions; -namespace Artemis.Plugins.Mqtt.Screens; +namespace Artemis.Plugins.Mqtt.ViewModels; /// /// ViewModel for single node edit dialog. /// -public class StructureNodeConfigurationDialogViewModel : DialogViewModelBase +public class StructureNodeConfigurationDialogViewModel : DialogViewModelBase { - private static readonly Type[] supportedTypes = { typeof(string), typeof(bool), typeof(int), typeof(double) }; + private string _label; + private Guid? _server; + private string _topic; + private Type _type; - private string label; - private Guid? server; - private string topic; - private Type type; - - public StructureNodeConfigurationDialogViewModel(PluginSettings settingsService, bool isGroup): this() + public StructureNodeConfigurationDialogViewModel(PluginSettings pluginSettings, StructureNodeViewModel poco) { - label = ""; - server = Guid.Empty; - topic = isGroup ? null : ""; - type = isGroup ? null : supportedTypes[0]; - IsGroup = isGroup; - ServerConnectionsSetting = settingsService.GetSetting>("ServerConnections"); - } + _label = poco.Label; + _server = poco.Server; + _topic = poco.Topic; + _type = poco.Type; + IsGroup = poco.IsGroup; + ServerConnectionsSetting = pluginSettings.GetSetting>("ServerConnections"); - public StructureNodeConfigurationDialogViewModel(PluginSettings settingsService, StructureNodeViewModel target) : this() - { - label = target.Label; - server = target.Server; - topic = target.Topic; - type = target.Type; - IsGroup = target.IsGroup; - ServerConnectionsSetting = settingsService.GetSetting>("ServerConnections"); - } - - private StructureNodeConfigurationDialogViewModel() - { Save = ReactiveCommand.Create(ExecuteSave); + Cancel = ReactiveCommand.Create(ExecuteCancel); this.ValidationRule(vm => vm.Label, label => !string.IsNullOrWhiteSpace(label), "Label cannot be empty"); this.ValidationRule(vm => vm.Server, server => server != null && server != Guid.Empty, "Server cannot be empty"); this.ValidationRule(vm => vm.Topic, topic => !string.IsNullOrWhiteSpace(topic), "Topic cannot be empty"); - + if (!IsGroup) { this.ValidationRule(vm => vm.Type, type => type != null, "Type cannot be empty"); @@ -55,47 +41,48 @@ private StructureNodeConfigurationDialogViewModel() } public ReactiveCommand Save { get; } - + public ReactiveCommand Cancel { get; } public string Label { - get => label; - set => RaiseAndSetIfChanged(ref label, value); + get => _label; + set => RaiseAndSetIfChanged(ref _label, value); } public Guid? Server { - get => server; - set => RaiseAndSetIfChanged(ref server, value); + get => _server; + set => RaiseAndSetIfChanged(ref _server, value); } public string Topic { - get => topic; - set => RaiseAndSetIfChanged(ref topic, value); + get => _topic; + set => RaiseAndSetIfChanged(ref _topic, value); } public Type Type { - get => type; - set => RaiseAndSetIfChanged(ref type, value); + get => _type; + set => RaiseAndSetIfChanged(ref _type, value); } - + public bool IsGroup { get; } public bool IsValue => !IsGroup; - public IEnumerable SupportedValueTypes => supportedTypes; + public IEnumerable SupportedValueTypes { get; } = new[] { typeof(string), typeof(bool), typeof(int), typeof(double) }; + public PluginSetting> ServerConnectionsSetting { get; } public void ExecuteSave() { if (!HasErrors) - Close(new StructureNodeConfigurationDialogResult(Label, Server, Topic, Type)); + Close(new(Label, Server, Topic, Type, IsGroup)); } -} -/// -/// POCO that contains the result of a successful MqttNodeConfiguration dialog. -/// -public record StructureNodeConfigurationDialogResult(string Label, Guid? Server, string Topic, Type Type); \ No newline at end of file + public void ExecuteCancel() + { + Close(null); + } +} \ No newline at end of file diff --git a/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeViewModel.cs b/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeViewModel.cs index 826f936..9d1c86f 100644 --- a/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeViewModel.cs +++ b/src/Artemis.Plugins.Mqtt/ViewModels/StructureNodeViewModel.cs @@ -3,96 +3,77 @@ using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; -using Artemis.Plugins.Mqtt.DataModels.Dynamic; +using Artemis.Plugins.Mqtt.DataModels; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; -namespace Artemis.Plugins.Mqtt.Screens; +namespace Artemis.Plugins.Mqtt.ViewModels; -/// -/// ViewModel representing a model. -/// public class StructureNodeViewModel : ViewModelBase { private readonly IWindowService _windowService; - private readonly StructureNodeViewModel parent; - - private string label; - private Guid? server; - private string topic; - private Type type; - - /// - /// Creates a new, blank ViewModel that represents a non-materialized . - /// - private StructureNodeViewModel(IWindowService windowService, StructureNodeViewModel parent) + private readonly StructureNodeViewModel? _parent; + + private string _label; + private Guid? _server; + private string _topic; + private Type _type; + + public StructureNodeViewModel(IWindowService windowService, StructureNodeViewModel? parent, StructureDefinitionNode model) { - this._windowService = windowService; - this.parent = parent; + _windowService = windowService; + _parent = parent; + + _label = model.Label; + _server = model.Server; + _topic = model.Topic; + _type = model.Type; + + Children = model.IsGroup + ? new ObservableCollection(model.Children!.Select(c => new StructureNodeViewModel(windowService, this, c))) + : new ObservableCollection(); } - /// - /// Creates a new ViewModel that represents the given . - /// - public StructureNodeViewModel(IWindowService windowService, StructureNodeViewModel parent, StructureDefinitionNode model) : this(windowService, - parent) - { - label = model.Label; - server = model.Server; - topic = model.Topic; - type = model.Type; - if (model.Children != null) - Children = new ObservableCollection( - model.Children.Select(c => new StructureNodeViewModel(windowService, this, c)) - ); - } - - /// - /// Converts this ViewModel into a model that can be saved, and used - /// by the . - /// public StructureDefinitionNode ViewModelToModel() { - return new StructureDefinitionNode() - { - Label = label, - Server = server, - Topic = topic, - Type = type, - Children = IsGroup ? new List(Children.Select(c => c.ViewModelToModel())) : null - }; + var node = new StructureDefinitionNode(Label, Server, Topic, Type, IsGroup); + + if (Children.Any()) + node.Children!.AddRange(Children.Select(c => c.ViewModelToModel())); + + return node; } #region Properties public string Label { - get => label; - set => RaiseAndSetIfChanged(ref label, value); + get => _label; + set => RaiseAndSetIfChanged(ref _label, value); } public Guid? Server { - get => server; - set => RaiseAndSetIfChanged(ref server, value); + get => _server; + set => RaiseAndSetIfChanged(ref _server, value); } public string Topic { - get => topic; - set => RaiseAndSetIfChanged(ref topic, value); + get => _topic; + set => RaiseAndSetIfChanged(ref _topic, value); } public Type Type { - get => type; - set => RaiseAndSetIfChanged(ref type, value); + get => _type; + set => RaiseAndSetIfChanged(ref _type, value); } public ObservableCollection Children { get; init; } - public bool IsGroup => Children != null; - public bool IsValue => Children == null; + public bool IsGroup => Children.Any(); + public bool IsValue => !IsGroup; #endregion @@ -103,7 +84,10 @@ public Type Type /// public async Task EditNode() { - var r = await _windowService.ShowDialogAsync(this); + var r = await _windowService.ShowDialogAsync(this); + if (r == null) + return; + Label = r.Label; Server = r.Server; Topic = r.Topic; @@ -115,9 +99,14 @@ public async Task EditNode() /// public async Task DeleteNode() { - // If Children is null or does not have this child, throw an error - if (parent.Children?.Contains(this) != true) - throw new InvalidOperationException("This node does not support child or child does not exist in this node."); + if(_parent == null) + throw new InvalidOperationException("Cannot delete root node."); + + if (!_parent.IsGroup) + throw new InvalidOperationException("Cannot delete child node from node that does not support children."); + + if (!_parent.Children.Contains(this)) + throw new InvalidOperationException("Cannot delete node that is not a child of this node."); var result = await _windowService.ShowConfirmContentDialog( $"Delete {(IsGroup ? "Group" : "Value")}", @@ -128,7 +117,7 @@ public async Task DeleteNode() "Don't delete" ); if (result) - parent.Children.Remove(this); + _parent.Children.Remove(this); } /// @@ -138,18 +127,18 @@ public async Task DeleteNode() /// If this node is a value-type node that does not support children. public async Task AddChildNode(bool isGroup) { - if (IsValue) + if (!IsGroup) throw new InvalidOperationException("Cannot add a child item to an item that does not support children."); - var r = await _windowService.ShowDialogAsync(isGroup); - Children.Add(new StructureNodeViewModel(_windowService, this) - { - Label = r.Label, - Server = isGroup ? null : r.Server, - Topic = isGroup ? null : r.Topic, - Type = isGroup ? null : r.Type, - Children = isGroup ? new ObservableCollection() : null - }); + var child = new StructureDefinitionNode("", Guid.Empty, "", typeof(string), isGroup); + var childVm = new StructureNodeViewModel(_windowService, this, child); + + var dialogResult = await _windowService.ShowDialogAsync(childVm); + + if (dialogResult is null) + return; + + Children.Add(childVm); } #endregion diff --git a/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml b/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml index eab3a60..b565a6d 100644 --- a/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml +++ b/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml @@ -4,13 +4,13 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:system="clr-namespace:System;assembly=netstandard" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" - xmlns:scr="clr-namespace:Artemis.Plugins.Mqtt.Screens" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" + xmlns:viewModels="clr-namespace:Artemis.Plugins.Mqtt.ViewModels" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="600" - x:Class="Artemis.Plugins.Mqtt.Screens.MqttPluginConfigurationView" - x:DataType="scr:MqttPluginConfigurationViewModel"> + x:Class="Artemis.Plugins.Mqtt.Views.MqttPluginConfigurationView" + x:DataType="viewModels:MqttPluginConfigurationViewModel"> True False @@ -77,7 +77,7 @@ Margin="0,8"> diff --git a/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml.cs b/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml.cs index 6cb9daf..2fb41d5 100644 --- a/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml.cs +++ b/src/Artemis.Plugins.Mqtt/Views/MqttPluginConfigurationView.axaml.cs @@ -1,6 +1,7 @@ -using Avalonia.ReactiveUI; +using Artemis.Plugins.Mqtt.ViewModels; +using Avalonia.ReactiveUI; -namespace Artemis.Plugins.Mqtt.Screens; +namespace Artemis.Plugins.Mqtt.Views; public partial class MqttPluginConfigurationView : ReactiveUserControl { diff --git a/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml b/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml index e0fcc5f..8b612ad 100644 --- a/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml +++ b/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml @@ -2,10 +2,10 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:local="clr-namespace:Artemis.Plugins.Mqtt.Screens" + xmlns:viewModels="clr-namespace:Artemis.Plugins.Mqtt.ViewModels" mc:Ignorable="d" Width="450" - x:Class="Artemis.Plugins.Mqtt.Screens.ServerConnectionDialogView" - x:DataType="local:ServerConnectionDialogViewModel"> + x:Class="Artemis.Plugins.Mqtt.Views.ServerConnectionDialogView" + x:DataType="viewModels:ServerConnectionDialogViewModel"> diff --git a/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml.cs b/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml.cs index 13e82c7..765c9c2 100644 --- a/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml.cs +++ b/src/Artemis.Plugins.Mqtt/Views/ServerConnectionDialogView.axaml.cs @@ -1,6 +1,7 @@ -using Artemis.UI.Shared; +using Artemis.Plugins.Mqtt.ViewModels; +using Artemis.UI.Shared; -namespace Artemis.Plugins.Mqtt.Screens; +namespace Artemis.Plugins.Mqtt.Views; public partial class ServerConnectionDialogView : ReactiveAppWindow { diff --git a/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml b/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml index 26e8414..315b5b0 100644 --- a/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml +++ b/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml @@ -4,9 +4,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" - xmlns:local="clr-namespace:Artemis.Plugins.Mqtt.Screens" - x:Class="Artemis.Plugins.Mqtt.Screens.StructureNodeConfigurationDialogView" - x:DataType="local:StructureNodeConfigurationDialogViewModel" + xmlns:viewModels="clr-namespace:Artemis.Plugins.Mqtt.ViewModels" + x:Class="Artemis.Plugins.Mqtt.Views.StructureNodeConfigurationDialogView" + x:DataType="viewModels:StructureNodeConfigurationDialogViewModel" mc:Ignorable="d" Width="450"> diff --git a/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml.cs b/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml.cs index a945d34..a2eb121 100644 --- a/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml.cs +++ b/src/Artemis.Plugins.Mqtt/Views/StructureNodeConfigurationDialogView.axaml.cs @@ -1,7 +1,7 @@ -using Artemis.UI.Shared; -using Avalonia.ReactiveUI; +using Artemis.Plugins.Mqtt.ViewModels; +using Artemis.UI.Shared; -namespace Artemis.Plugins.Mqtt.Screens; +namespace Artemis.Plugins.Mqtt.Views; public partial class StructureNodeConfigurationDialogView : ReactiveAppWindow { diff --git a/src/Artemis.Plugins.Mqtt/plugin.json b/src/Artemis.Plugins.Mqtt/plugin.json index 1325e72..327dbfd 100644 --- a/src/Artemis.Plugins.Mqtt/plugin.json +++ b/src/Artemis.Plugins.Mqtt/plugin.json @@ -5,6 +5,6 @@ "Author": "Wibble199 & diogotr7", "Icon": "AccessPoint", "Description": "Provides a customisable data model based on data received by subscribing to an MQTT broker.", - "Version": "1.0.0.0", + "Version": "1.1.0.0", "Main": "Artemis.Plugins.Mqtt.dll" } \ No newline at end of file