diff --git a/docs/articles/module-resources/resource-object-graph.md b/docs/articles/module-resources/resource-object-graph.md index 5b6a5e3d5..e8746f290 100644 --- a/docs/articles/module-resources/resource-object-graph.md +++ b/docs/articles/module-resources/resource-object-graph.md @@ -84,3 +84,25 @@ public class DynamicTree : Resource ``` The `Shrink`-method shows the two alternatives for destroying resource instances. The caller can specify whether to remove the object by flagging it as deleted or actually deleting the entry from the database. The call with a single argument is a shortcut for the second one with permanent = false. In both cases the object is removed from the resource graph and all references it occurs in to allow proper garbage collection. + +### Notify without saving + +In some cases a resource property changes frequently at runtime but does not need to be persisted — for example, a counter or a current temperature. For these transient changes, `RaiseResourceChanged(false)` raises the `ResourceChanged` event on the `IResourceManagement` facade without triggering a database persistence. +An optional property name is captured automatically when called from a property setter via `[CallerMemberName]`. + +```cs +public class MonitoredCell : Resource +{ + private int _partCount; + + public int PartCount + { + get => _partCount; + set + { + _partCount = value; + RaiseResourceChanged(save: false); // PropertyName = "PartCount" + } + } +} +``` diff --git a/src/Moryx.AbstractionLayer/Resources/Resource.cs b/src/Moryx.AbstractionLayer/Resources/Resource.cs index b09c619af..1327cb533 100644 --- a/src/Moryx.AbstractionLayer/Resources/Resource.cs +++ b/src/Moryx.AbstractionLayer/Resources/Resource.cs @@ -1,6 +1,7 @@ // Copyright (c) 2026 Phoenix Contact GmbH & Co. KG // Licensed under the Apache License, Version 2.0 +using System.Runtime.CompilerServices; using System.Runtime.Serialization; using Microsoft.Extensions.Logging; using Moryx.AbstractionLayer.Capabilities; @@ -138,10 +139,27 @@ protected virtual void OnDispose() /// Inform the resource management, that this instance was modified /// and trigger saving the current state to storage /// - protected void RaiseResourceChanged() + protected void RaiseResourceChanged() => RaiseResourceChanged(true); + + /// + /// Inform the resource management, that this instance was modified + /// + /// If true, the change is persisted to storage. + /// Name of the property that changed. + protected void RaiseResourceChanged(bool save, [CallerMemberName] string propertyName = null) { - // This is only null during boot, when the resource manager populates the object - Changed?.Invoke(this, EventArgs.Empty); + if (save) + { + Changed?.Invoke(this, EventArgs.Empty); + } + else + { + Notified?.Invoke(this, new ResourceChangedEventArgs + { + Save = save, + PropertyName = propertyName + }); + } } /// @@ -169,9 +187,16 @@ protected set /// public event EventHandler CapabilitiesChanged; + // TODO: In next major, merge Changed and Notified into a single event using ResourceChangedEventArgs /// /// Event raised when the resource was modified and the changes should be /// written to the data storage /// public event EventHandler Changed; -} \ No newline at end of file + + /// + /// Event raised when the resource wants to notify listeners of a change + /// without triggering persistence + /// + public event EventHandler Notified; +} diff --git a/src/Moryx.AbstractionLayer/Resources/ResourceChangedEventArgs.cs b/src/Moryx.AbstractionLayer/Resources/ResourceChangedEventArgs.cs new file mode 100644 index 000000000..67949db42 --- /dev/null +++ b/src/Moryx.AbstractionLayer/Resources/ResourceChangedEventArgs.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2026 Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +namespace Moryx.AbstractionLayer.Resources; + +/// +/// Event args for +/// +public class ResourceChangedEventArgs : EventArgs +{ + /// + /// If true, the resource will be saved to storage. + /// + public bool Save { get; init; } + + /// + /// Name of the property that changed, or null if not specified + /// + public string PropertyName { get; init; } +} diff --git a/src/Moryx.FactoryMonitor.Endpoints/Extensions/FactoryMonitorHelper.cs b/src/Moryx.FactoryMonitor.Endpoints/Extensions/FactoryMonitorHelper.cs index 1916f8eeb..7bd53644e 100644 --- a/src/Moryx.FactoryMonitor.Endpoints/Extensions/FactoryMonitorHelper.cs +++ b/src/Moryx.FactoryMonitor.Endpoints/Extensions/FactoryMonitorHelper.cs @@ -62,6 +62,21 @@ public static void ActivityUpdated(ActivityUpdatedEventArgs activityEventArg, Li broadcast(Cell_State_Event_Type_Key, cellStateChangedModel); } + // ToDo: Added and removed resources not reflected + public static void ResourceUpdated(IResource changedResource, + Dictionary locationToCellMappings, + Converter.Converter converter, + IResourceManagement resourceManager, + Action broadcast) + { + var mapping = locationToCellMappings.FirstOrDefault(l2c => l2c.Value.Id == changedResource.Id); + if (mapping.Key is null) + return; + + var resourceChangedModel = mapping.Value.GetResourceChangedModel(converter, resourceManager, mapping.Key); + broadcast(Recource_Event_Type_Key, resourceChangedModel); + } + public static void ResourceUpdated(IResourceManagement resourceManager, Func, Dictionary> mapCellsTo, Converter.Converter converter, diff --git a/src/Moryx.FactoryMonitor.Endpoints/FactoryMonitorController.cs b/src/Moryx.FactoryMonitor.Endpoints/FactoryMonitorController.cs index f2d7a3ebc..0d4312c6f 100644 --- a/src/Moryx.FactoryMonitor.Endpoints/FactoryMonitorController.cs +++ b/src/Moryx.FactoryMonitor.Endpoints/FactoryMonitorController.cs @@ -246,7 +246,10 @@ public async Task FactoryStatesStream(CancellationToken cancellationToken) var converter = new Converter.Converter(_serialization, _logger); // Define event handlers using helper methods - var resourceEventHandler = new ElapsedEventHandler((_, _) => + var resourceChangedEventHandler = new EventHandler((_, resource) => + FactoryMonitorHelper.ResourceUpdated(resource, _locationToCellMappings, converter, _resourceManager, Broadcast)); + + var resourceTimerEventHandler = new ElapsedEventHandler((_, _) => FactoryMonitorHelper.ResourceUpdated(_resourceManager, l => MapCellsTo(l), converter, Broadcast)); var capabilitiesEventHandler = new EventHandler((sender, _) => @@ -263,7 +266,7 @@ public async Task FactoryStatesStream(CancellationToken cancellationToken) FactoryMonitorHelper.ActivityUpdated(eventArgs, [.. _locationToCellMappings.Values], TryGetOrders(), Broadcast)); - // Setup timer + // TODO: Remove timer in next major when resources use RaiseResourceChanged _resourceChangedTimer = new(); _resourceChangedTimer.Interval = 5000; _resourceChangedTimer.AutoReset = true; @@ -279,10 +282,11 @@ public async Task FactoryStatesStream(CancellationToken cancellationToken) l2cMapping.Value.CapabilitiesChanged += capabilitiesEventHandler; } + _resourceManager.ResourceChanged += resourceChangedEventHandler; + _resourceChangedTimer.Elapsed += resourceTimerEventHandler; _orderManager.OperationStarted += orderStartedEventHandler; _orderManager.OperationUpdated += orderEventHandler; _processControl.ActivityUpdated += activityEventHandler; - _resourceChangedTimer.Elapsed += resourceEventHandler; await result.ExecuteAsync(HttpContext); } @@ -298,10 +302,11 @@ public async Task FactoryStatesStream(CancellationToken cancellationToken) l2cMapping.Value.CapabilitiesChanged -= capabilitiesEventHandler; } + _resourceManager.ResourceChanged -= resourceChangedEventHandler; + _resourceChangedTimer.Elapsed -= resourceTimerEventHandler; _orderManager.OperationStarted -= orderStartedEventHandler; _orderManager.OperationUpdated -= orderEventHandler; _processControl.ActivityUpdated -= activityEventHandler; - _resourceChangedTimer.Elapsed -= resourceEventHandler; _resourceChangedTimer?.Dispose(); } diff --git a/src/Moryx.FactoryMonitor.Web/app/src/app/components/cell-details/cell-details.ts b/src/Moryx.FactoryMonitor.Web/app/src/app/components/cell-details/cell-details.ts index 12b42eb63..0a9fafdc6 100644 --- a/src/Moryx.FactoryMonitor.Web/app/src/app/components/cell-details/cell-details.ts +++ b/src/Moryx.FactoryMonitor.Web/app/src/app/components/cell-details/cell-details.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0 */ -import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, computed, ChangeDetectionStrategy } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { CellImageDialog } from '@app/dialogs/cell-image-dialog/cell-image-dialog'; import { CellStoreService } from '@app/services/cell-store.service'; @@ -34,9 +34,17 @@ export class CellDetails { private matDialog = inject(MatDialog); private cellStoreService = inject(CellStoreService); - protected cellDetails = this.cellStoreService.cellSelected; protected TranslationConstants = TranslationConstants; + protected cellDetails = computed(() => { + const selected = this.cellStoreService.cellSelected(); + const updated = this.cellStoreService.cellUpdated(); + if (selected && updated && selected.id === updated.id) { + return updated; + } + return selected; + }); + protected openCellImageDialog() { this.matDialog.open(CellImageDialog, { data: { diff --git a/src/Moryx.Resources.Management/Resources/ResourceManager.cs b/src/Moryx.Resources.Management/Resources/ResourceManager.cs index 63fc31ade..17dc73614 100644 --- a/src/Moryx.Resources.Management/Resources/ResourceManager.cs +++ b/src/Moryx.Resources.Management/Resources/ResourceManager.cs @@ -260,6 +260,7 @@ private async Task InitializeAndStart(Resource resource) private void RegisterEvents(Resource instance) { instance.Changed += OnResourceChanged; + instance.Notified += OnResourceNotified; instance.CapabilitiesChanged += RaiseCapabilitiesChanged; foreach (var autoSaveCollection in ResourceReferenceTools.GetAutoSaveCollections(instance)) @@ -272,6 +273,7 @@ private void RegisterEvents(Resource instance) private void UnregisterEvents(Resource instance) { instance.Changed -= OnResourceChanged; + instance.Notified -= OnResourceNotified; instance.CapabilitiesChanged -= RaiseCapabilitiesChanged; foreach (var autoSaveCollection in ResourceReferenceTools.GetAutoSaveCollections(instance)) @@ -287,6 +289,14 @@ private void OnResourceChanged(object sender, EventArgs eventArgs) _ = Task.Run(() => SaveAsync((Resource)sender)); } + /// + /// Event handler when a resource notifies of a change without requiring persistence + /// + private void OnResourceNotified(object sender, ResourceChangedEventArgs eventArgs) + { + _ = Task.Run(() => RaiseResourceChanged((IResource)sender)); + } + /// /// Build object graph from simplified and flat resource list ///