Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions docs/articles/module-resources/resource-object-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
```
33 changes: 29 additions & 4 deletions src/Moryx.AbstractionLayer/Resources/Resource.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
/// </summary>
protected void RaiseResourceChanged()
protected void RaiseResourceChanged() => RaiseResourceChanged(true);

/// <summary>
/// Inform the resource management, that this instance was modified
/// </summary>
/// <param name="save">If true, the change is persisted to storage.</param>
/// <param name="propertyName">Name of the property that changed.</param>
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
});
}
}

/// <summary>
Expand Down Expand Up @@ -169,9 +187,16 @@ protected set
/// </summary>
public event EventHandler<ICapabilities> CapabilitiesChanged;

// TODO: In next major, merge Changed and Notified into a single event using ResourceChangedEventArgs
/// <summary>
/// Event raised when the resource was modified and the changes should be
/// written to the data storage
/// </summary>
public event EventHandler Changed;
}

/// <summary>
/// Event raised when the resource wants to notify listeners of a change
/// without triggering persistence
/// </summary>
public event EventHandler<ResourceChangedEventArgs> Notified;
}
20 changes: 20 additions & 0 deletions src/Moryx.AbstractionLayer/Resources/ResourceChangedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2026 Phoenix Contact GmbH & Co. KG
// Licensed under the Apache License, Version 2.0

namespace Moryx.AbstractionLayer.Resources;

/// <summary>
/// Event args for <see cref="Resource.Changed"/>
/// </summary>
public class ResourceChangedEventArgs : EventArgs
{
/// <summary>
/// If true, the resource will be saved to storage.
/// </summary>
public bool Save { get; init; }

/// <summary>
/// Name of the property that changed, or null if not specified
/// </summary>
public string PropertyName { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<IMachineLocation, ICell> locationToCellMappings,
Converter.Converter converter,
IResourceManagement resourceManager,
Action<string, object> 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<IEnumerable<IMachineLocation>, Dictionary<IMachineLocation, ICell>> mapCellsTo,
Converter.Converter converter,
Expand Down
13 changes: 9 additions & 4 deletions src/Moryx.FactoryMonitor.Endpoints/FactoryMonitorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IResource>((_, resource) =>
FactoryMonitorHelper.ResourceUpdated(resource, _locationToCellMappings, converter, _resourceManager, Broadcast));

var resourceTimerEventHandler = new ElapsedEventHandler((_, _) =>
FactoryMonitorHelper.ResourceUpdated(_resourceManager, l => MapCellsTo(l), converter, Broadcast));

var capabilitiesEventHandler = new EventHandler<ICapabilities>((sender, _) =>
Expand All @@ -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;
Expand All @@ -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);
}
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand Down
10 changes: 10 additions & 0 deletions src/Moryx.Resources.Management/Resources/ResourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -287,6 +289,14 @@ private void OnResourceChanged(object sender, EventArgs eventArgs)
_ = Task.Run(() => SaveAsync((Resource)sender));
}

/// <summary>
/// Event handler when a resource notifies of a change without requiring persistence
/// </summary>
private void OnResourceNotified(object sender, ResourceChangedEventArgs eventArgs)
{
_ = Task.Run(() => RaiseResourceChanged((IResource)sender));
}

/// <summary>
/// Build object graph from simplified <see cref="ResourceEntityAccessor"/> and flat resource list
/// </summary>
Expand Down
Loading