Testing und interfaces

This commit is contained in:
Meik
2026-02-13 09:06:36 +01:00
parent abbce22aa9
commit 352dc42ae7
8 changed files with 846 additions and 166 deletions

View File

@@ -0,0 +1,150 @@
using FasdDesktopUi.Basics.Services.Models;
namespace F4SD.Cockpit.Client.Test.Basics.Sevices.TicketOverview;
public class TicketOverviewCountProcessorTest
{
private static readonly string[] OverviewKeys = ["TicketsNew", "IncidentNew"];
[Fact]
public void Calculate_PersonalScope_UpdatesOnlyPersonalAndReturnsDeltaChanges()
{
// Arrange
Dictionary<string, TileCounts> current = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = new TileCounts(1, 7),
["IncidentNew"] = new TileCounts(2, 8)
};
Dictionary<string, int> incoming = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 5,
["IncidentNew"] = 2
};
// Act
ScopeCountProcessingResult result = TicketOverviewCountProcessor.Calculate(
current,
OverviewKeys,
TileScope.Personal,
incoming,
hasInitializedScope: true);
// Assert
Assert.False(result.IsInitialization);
Assert.Equal(5, result.UpdatedCounts["TicketsNew"].Personal);
Assert.Equal(7, result.UpdatedCounts["TicketsNew"].Role);
Assert.Equal(2, result.UpdatedCounts["IncidentNew"].Personal);
Assert.Equal(8, result.UpdatedCounts["IncidentNew"].Role);
Assert.Single(result.Changes);
TileCountChange change = result.Changes[0];
Assert.Equal("TicketsNew", change.Key);
Assert.Equal(TileScope.Personal, change.Scope);
Assert.Equal(1, change.OldCount);
Assert.Equal(5, change.NewCount);
Assert.Equal(4, change.Delta);
}
[Fact]
public void Calculate_RoleScope_UpdatesOnlyRoleAndReturnsDeltaChanges()
{
// Arrange
Dictionary<string, TileCounts> current = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = new TileCounts(10, 3),
["IncidentNew"] = new TileCounts(4, 6)
};
Dictionary<string, int> incoming = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 3,
["IncidentNew"] = 9
};
// Act
ScopeCountProcessingResult result = TicketOverviewCountProcessor.Calculate(
current,
OverviewKeys,
TileScope.Role,
incoming,
hasInitializedScope: true);
// Assert
Assert.False(result.IsInitialization);
Assert.Equal(10, result.UpdatedCounts["TicketsNew"].Personal);
Assert.Equal(3, result.UpdatedCounts["TicketsNew"].Role);
Assert.Equal(4, result.UpdatedCounts["IncidentNew"].Personal);
Assert.Equal(9, result.UpdatedCounts["IncidentNew"].Role);
Assert.Single(result.Changes);
TileCountChange change = result.Changes[0];
Assert.Equal("IncidentNew", change.Key);
Assert.Equal(TileScope.Role, change.Scope);
Assert.Equal(6, change.OldCount);
Assert.Equal(9, change.NewCount);
Assert.Equal(3, change.Delta);
}
[Fact]
public void Calculate_FirstInitialization_DoesNotReturnDeltaChanges()
{
// Arrange
Dictionary<string, TileCounts> current = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = new TileCounts(0, 0),
["IncidentNew"] = new TileCounts(0, 0)
};
Dictionary<string, int> incoming = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 4,
["IncidentNew"] = 1
};
// Act
ScopeCountProcessingResult result = TicketOverviewCountProcessor.Calculate(
current,
OverviewKeys,
TileScope.Personal,
incoming,
hasInitializedScope: false);
// Assert
Assert.True(result.IsInitialization);
Assert.Empty(result.Changes);
Assert.Equal(4, result.UpdatedCounts["TicketsNew"].Personal);
Assert.Equal(1, result.UpdatedCounts["IncidentNew"].Personal);
}
[Fact]
public void Calculate_MissingIncomingValues_DefaultsToZero()
{
// Arrange
Dictionary<string, TileCounts> current = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = new TileCounts(5, 1),
["IncidentNew"] = new TileCounts(2, 4)
};
Dictionary<string, int> incoming = new(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 5
};
// Act
ScopeCountProcessingResult result = TicketOverviewCountProcessor.Calculate(
current,
OverviewKeys,
TileScope.Personal,
incoming,
hasInitializedScope: true);
// Assert
Assert.Equal(5, result.UpdatedCounts["TicketsNew"].Personal);
Assert.Equal(1, result.UpdatedCounts["TicketsNew"].Role);
Assert.Equal(0, result.UpdatedCounts["IncidentNew"].Personal);
Assert.Equal(4, result.UpdatedCounts["IncidentNew"].Role);
Assert.Single(result.Changes);
Assert.Equal("IncidentNew", result.Changes[0].Key);
Assert.Equal(2, result.Changes[0].OldCount);
Assert.Equal(0, result.Changes[0].NewCount);
}
}

View File

@@ -0,0 +1,243 @@
using FasdDesktopUi.Basics.Services;
using FasdDesktopUi.Basics.Services.Models;
namespace F4SD.Cockpit.Client.Test.Basics.Sevices.TicketOverview;
public class TicketOverviewUpdateServiceTest
{
[Fact]
public async Task UpdateAvailability_FirstFetch_EmitsInitializationEventsForBothScopes()
{
// Arrange
var communication = new FakeCommunication();
communication.SetCounts(TileScope.Personal, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 2
});
communication.SetCounts(TileScope.Role, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 5
});
var service = CreateService(communication, out _);
var events = new List<TicketOverviewCountsChangedEventArgs>();
service.OverviewCountsChanged += (_, args) => events.Add(args);
// Act
service.UpdateAvailability(true);
await WaitUntilAsync(() => service.AreAllScopesInitialized);
// Assert
Assert.Equal(2, events.Count);
Assert.All(events, e => Assert.Empty(e.Changes));
Assert.Contains(events, e => e.InitializedScope == TileScope.Personal);
Assert.Contains(events, e => e.InitializedScope == TileScope.Role);
Assert.Equal(2, service.CurrentCounts["TicketsNew"].Personal);
Assert.Equal(5, service.CurrentCounts["TicketsNew"].Role);
}
[Fact]
public async Task FetchAsync_ChangedAndUnchangedCounts_EmitsOnlyRealDeltaEvent()
{
// Arrange
var communication = new FakeCommunication();
communication.SetCounts(TileScope.Personal, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 1
});
communication.SetCounts(TileScope.Role, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 3
});
var service = CreateService(communication, out _);
var events = new List<TicketOverviewCountsChangedEventArgs>();
service.OverviewCountsChanged += (_, args) => events.Add(args);
service.UpdateAvailability(true);
await WaitUntilAsync(() => service.AreAllScopesInitialized);
events.Clear();
// Act + Assert (changed)
communication.SetCounts(TileScope.Personal, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 4
});
await service.FetchAsync(TileScope.Personal);
Assert.Single(events);
Assert.Single(events[0].Changes);
Assert.Equal("TicketsNew", events[0].Changes[0].Key);
Assert.Equal(1, events[0].Changes[0].OldCount);
Assert.Equal(4, events[0].Changes[0].NewCount);
Assert.Equal(3, events[0].Changes[0].Delta);
// Act + Assert (unchanged)
events.Clear();
await service.FetchAsync(TileScope.Personal);
Assert.Empty(events);
}
[Fact]
public async Task Stop_ResetsInitializationAndCounts()
{
// Arrange
var communication = new FakeCommunication();
communication.SetCounts(TileScope.Personal, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["TicketsNew"] = 7
});
communication.SetCounts(TileScope.Role, new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["IncidentNew"] = 9
});
var service = CreateService(communication, out _);
service.UpdateAvailability(true);
await WaitUntilAsync(() => service.AreAllScopesInitialized);
// Act
service.Stop();
// Assert
Assert.False(service.IsScopeInitialized(TileScope.Personal));
Assert.False(service.IsScopeInitialized(TileScope.Role));
Assert.All(service.CurrentCounts.Values, value =>
{
Assert.Equal(0, value.Personal);
Assert.Equal(0, value.Role);
});
}
private static TicketOverviewUpdateService CreateService(FakeCommunication communication, out FakeDispatcher dispatcher)
{
dispatcher = new FakeDispatcher();
var source = new FakeCommunicationSource(communication);
var settings = new FakeSettingsProvider();
return new TicketOverviewUpdateService(source, dispatcher, settings);
}
private static async Task WaitUntilAsync(Func<bool> condition, int timeoutMs = 2000)
{
var startedAt = DateTime.UtcNow;
while (!condition())
{
if ((DateTime.UtcNow - startedAt).TotalMilliseconds > timeoutMs)
{
throw new TimeoutException("Condition was not reached within timeout.");
}
await Task.Delay(10);
}
}
private sealed class FakeCommunicationSource : ITicketOverviewCommunicationSource
{
private readonly ITicketOverviewCommunication _communication;
public FakeCommunicationSource(ITicketOverviewCommunication communication)
{
_communication = communication;
}
public ITicketOverviewCommunication Resolve()
{
return _communication;
}
}
private sealed class FakeCommunication : ITicketOverviewCommunication
{
private Dictionary<string, int> _personalCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private Dictionary<string, int> _roleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
public bool IsDemo()
{
return false;
}
public Task<Dictionary<string, int>> GetTicketOverviewCounts(string[] overviewKeys, bool useRoleScope)
{
var source = useRoleScope ? _roleCounts : _personalCounts;
return Task.FromResult(new Dictionary<string, int>(source, StringComparer.OrdinalIgnoreCase));
}
public void RegisterGeneratedTicket(FasdCockpitCommunicationDemo.DemoTicketRecord record)
{
}
public void SetCounts(TileScope scope, Dictionary<string, int> counts)
{
var copy = counts == null
? new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, int>(counts, StringComparer.OrdinalIgnoreCase);
if (scope == TileScope.Role)
{
_roleCounts = copy;
}
else
{
_personalCounts = copy;
}
}
}
private sealed class FakeDispatcher : ITicketOverviewDispatcher
{
public Task InvokeAsync(Action action)
{
action();
return Task.CompletedTask;
}
public Task InvokeAsync(Func<Task> action)
{
return action();
}
public ITicketOverviewTimer CreateTimer(TimeSpan interval, Action tick)
{
return new FakeTimer(interval, tick);
}
}
private sealed class FakeSettingsProvider : ITicketOverviewSettingsProvider
{
public int GetPollingMinutes(TileScope scope)
{
return 1;
}
}
private sealed class FakeTimer : ITicketOverviewTimer
{
private readonly Action _tick;
public FakeTimer(TimeSpan interval, Action tick)
{
Interval = interval;
_tick = tick;
}
public TimeSpan Interval { get; set; }
public bool IsEnabled { get; private set; }
public void Start()
{
IsEnabled = true;
}
public void Stop()
{
IsEnabled = false;
}
public void Fire()
{
_tick?.Invoke();
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using C4IT.FASD.Cockpit.Communication;
#if isDemo
using FasdCockpitCommunicationDemo;
#endif
namespace FasdDesktopUi.Basics.Services.Models
{
internal interface ITicketOverviewCommunication
{
bool IsDemo();
Task<Dictionary<string, int>> GetTicketOverviewCounts(string[] overviewKeys, bool useRoleScope);
#if isDemo
void RegisterGeneratedTicket(DemoTicketRecord record);
#endif
}
internal interface ITicketOverviewCommunicationSource
{
ITicketOverviewCommunication Resolve();
}
internal sealed class TicketOverviewCommunicationSource : ITicketOverviewCommunicationSource
{
public ITicketOverviewCommunication Resolve()
{
var communication = cFasdCockpitCommunicationBase.Instance;
return communication == null ? null : new TicketOverviewCommunicationAdapter(communication);
}
}
internal sealed class TicketOverviewCommunicationAdapter : ITicketOverviewCommunication
{
private readonly cFasdCockpitCommunicationBase _communication;
internal TicketOverviewCommunicationAdapter(cFasdCockpitCommunicationBase communication)
{
_communication = communication ?? throw new ArgumentNullException(nameof(communication));
}
public bool IsDemo()
{
return _communication.IsDemo();
}
public async Task<Dictionary<string, int>> GetTicketOverviewCounts(string[] overviewKeys, bool useRoleScope)
{
var rawCounts = await _communication.GetTicketOverviewCounts(overviewKeys, useRoleScope).ConfigureAwait(false);
return rawCounts == null
? new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, int>(rawCounts, StringComparer.OrdinalIgnoreCase);
}
#if isDemo
public void RegisterGeneratedTicket(DemoTicketRecord record)
{
var demoCommunication = _communication as cFasdCockpitCommunicationDemo;
demoCommunication?.RegisterGeneratedTicket(record);
}
#endif
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
namespace FasdDesktopUi.Basics.Services.Models
{
internal static class TicketOverviewCountProcessor
{
internal static ScopeCountProcessingResult Calculate(
IReadOnlyDictionary<string, TileCounts> currentCounts,
IEnumerable<string> overviewKeys,
TileScope scope,
IDictionary<string, int> incomingCounts,
bool hasInitializedScope)
{
if (overviewKeys == null)
throw new ArgumentNullException(nameof(overviewKeys));
var updatedCounts = new Dictionary<string, TileCounts>(StringComparer.OrdinalIgnoreCase);
if (currentCounts != null)
{
foreach (var kvp in currentCounts)
{
updatedCounts[kvp.Key] = kvp.Value;
}
}
var changes = new List<TileCountChange>();
foreach (var key in overviewKeys)
{
var previous = currentCounts != null && currentCounts.TryGetValue(key, out var counts)
? counts
: TileCounts.Empty;
var incoming = incomingCounts != null && incomingCounts.TryGetValue(key, out var value)
? value
: 0;
TileCounts updated;
int oldValue;
if (scope == TileScope.Role)
{
updated = new TileCounts(previous.Personal, incoming);
oldValue = previous.Role;
}
else
{
updated = new TileCounts(incoming, previous.Role);
oldValue = previous.Personal;
}
updatedCounts[key] = updated;
if (hasInitializedScope && oldValue != incoming)
changes.Add(new TileCountChange(key, scope, oldValue, incoming));
}
return new ScopeCountProcessingResult(updatedCounts, changes, !hasInitializedScope);
}
}
internal sealed class ScopeCountProcessingResult
{
internal ScopeCountProcessingResult(
IReadOnlyDictionary<string, TileCounts> updatedCounts,
IReadOnlyList<TileCountChange> changes,
bool isInitialization)
{
UpdatedCounts = updatedCounts ?? throw new ArgumentNullException(nameof(updatedCounts));
Changes = changes ?? Array.Empty<TileCountChange>();
IsInitialization = isInitialization;
}
internal IReadOnlyDictionary<string, TileCounts> UpdatedCounts { get; }
internal IReadOnlyList<TileCountChange> Changes { get; }
internal bool IsInitialization { get; }
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace FasdDesktopUi.Basics.Services.Models
{
internal interface ITicketOverviewTimer
{
TimeSpan Interval { get; set; }
bool IsEnabled { get; }
void Start();
void Stop();
}
internal interface ITicketOverviewDispatcher
{
Task InvokeAsync(Action action);
Task InvokeAsync(Func<Task> action);
ITicketOverviewTimer CreateTimer(TimeSpan interval, Action tick);
}
internal sealed class TicketOverviewDispatcher : ITicketOverviewDispatcher
{
private readonly Dispatcher _dispatcher;
internal TicketOverviewDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
public Task InvokeAsync(Action action)
{
return _dispatcher.InvokeAsync(action, DispatcherPriority.Background).Task;
}
public Task InvokeAsync(Func<Task> action)
{
return _dispatcher.InvokeAsync(action, DispatcherPriority.Background).Task.Unwrap();
}
public ITicketOverviewTimer CreateTimer(TimeSpan interval, Action tick)
{
var timer = new DispatcherTimer(interval, DispatcherPriority.Background, (sender, args) => tick?.Invoke(), _dispatcher)
{
IsEnabled = false
};
return new TicketOverviewTimer(timer);
}
}
internal sealed class TicketOverviewTimer : ITicketOverviewTimer
{
private readonly DispatcherTimer _timer;
internal TicketOverviewTimer(DispatcherTimer timer)
{
_timer = timer ?? throw new ArgumentNullException(nameof(timer));
}
public TimeSpan Interval
{
get => _timer.Interval;
set => _timer.Interval = value;
}
public bool IsEnabled => _timer.IsEnabled;
public void Start()
{
_timer.Start();
}
public void Stop()
{
_timer.Stop();
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Diagnostics;
using C4IT.FASD.Base;
namespace FasdDesktopUi.Basics.Services.Models
{
internal interface ITicketOverviewSettingsProvider
{
int GetPollingMinutes(TileScope scope);
}
internal sealed class TicketOverviewSettingsProvider : ITicketOverviewSettingsProvider
{
public int GetPollingMinutes(TileScope scope)
{
int minutes = scope == TileScope.Role
? cF4sdTicketConfig.DefaultOverviewPollingRole
: cF4sdTicketConfig.DefaultOverviewPollingPersonal;
try
{
var ticketConfig = cFasdCockpitConfig.Instance?.Global?.TicketConfiguration;
if (ticketConfig != null)
{
minutes = scope == TileScope.Role
? ticketConfig.OverviewPollingRole
: ticketConfig.OverviewPollingPersonal;
}
}
catch (Exception ex)
{
Debug.WriteLine($"[TicketOverview] Settings fallback to defaults: {ex.Message}");
}
if (minutes < 1)
{
minutes = 1;
}
return minutes;
}
}
}

View File

@@ -1,13 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
using C4IT.FASD.Base; using C4IT.FASD.Base;
using C4IT.FASD.Cockpit.Communication; using C4IT.FASD.Cockpit.Communication;
using FasdDesktopUi;
using FasdDesktopUi.Basics.Models; using FasdDesktopUi.Basics.Models;
using FasdDesktopUi.Basics.Services.Models; using FasdDesktopUi.Basics.Services.Models;
#if isDemo #if isDemo
@@ -20,6 +19,9 @@ namespace FasdDesktopUi.Basics.Services
{ {
public sealed class TicketOverviewUpdateService public sealed class TicketOverviewUpdateService
{ {
#region Fields
private readonly ITicketOverviewCommunicationSource _communicationSource;
private static readonly string[] OverviewKeys = new[] private static readonly string[] OverviewKeys = new[]
{ {
"TicketsNew", "TicketsNew",
@@ -33,15 +35,16 @@ namespace FasdDesktopUi.Basics.Services
"UnassignedTickets", "UnassignedTickets",
"UnassignedTicketsCritical" "UnassignedTicketsCritical"
}; };
private readonly Dispatcher _dispatcher; private readonly ITicketOverviewDispatcher _dispatcher;
private readonly ITicketOverviewSettingsProvider _settingsProvider;
private readonly Dictionary<string, TileCounts> _currentCounts = new Dictionary<string, TileCounts>(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, TileCounts> _currentCounts = new Dictionary<string, TileCounts>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<(string Key, bool UseRoleScope), List<cF4sdApiSearchResultRelation>> _demoRelations = new Dictionary<(string, bool), List<cF4sdApiSearchResultRelation>>(); private readonly Dictionary<(string Key, bool UseRoleScope), List<cF4sdApiSearchResultRelation>> _demoRelations = new Dictionary<(string, bool), List<cF4sdApiSearchResultRelation>>();
private readonly HashSet<TileScope> _pendingScopes = new HashSet<TileScope>(); private readonly HashSet<TileScope> _pendingScopes = new HashSet<TileScope>();
private readonly HashSet<TileScope> _initializedScopes = new HashSet<TileScope>(); private readonly HashSet<TileScope> _initializedScopes = new HashSet<TileScope>();
private readonly object _fetchLock = new object(); private readonly object _fetchLock = new object();
private readonly HashSet<TileScope> _retryScopes = new HashSet<TileScope>(); private readonly HashSet<TileScope> _retryScopes = new HashSet<TileScope>();
private DispatcherTimer _personalTimer; private ITicketOverviewTimer _personalTimer;
private DispatcherTimer _roleTimer; private ITicketOverviewTimer _roleTimer;
private Task _fetchWorker; private Task _fetchWorker;
private bool _retryScheduled; private bool _retryScheduled;
private bool _isDemo; private bool _isDemo;
@@ -54,30 +57,53 @@ namespace FasdDesktopUi.Basics.Services
private readonly HashSet<string> _usedSummaries = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _usedSummaries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private const int SimulationHotkeyDelayMs = 400; private const int SimulationHotkeyDelayMs = 400;
private int _pendingSimulations; private int _pendingSimulations;
private DispatcherTimer _simulationFlushTimer; private ITicketOverviewTimer _simulationFlushTimer;
#endif #endif
#endregion
#region Construction and Singleton
private TicketOverviewUpdateService() private TicketOverviewUpdateService()
: this(
new TicketOverviewCommunicationSource(),
new TicketOverviewDispatcher(Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher),
new TicketOverviewSettingsProvider())
{ {
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; }
internal TicketOverviewUpdateService(
ITicketOverviewCommunicationSource communicationSource,
ITicketOverviewDispatcher dispatcher)
: this(communicationSource, dispatcher, new TicketOverviewSettingsProvider())
{
}
internal TicketOverviewUpdateService(
ITicketOverviewCommunicationSource communicationSource,
ITicketOverviewDispatcher dispatcher,
ITicketOverviewSettingsProvider settingsProvider)
{
_communicationSource = communicationSource ?? throw new ArgumentNullException(nameof(communicationSource));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_settingsProvider = settingsProvider ?? throw new ArgumentNullException(nameof(settingsProvider));
foreach (var key in OverviewKeys) foreach (var key in OverviewKeys)
{ {
_currentCounts[key] = TileCounts.Empty; _currentCounts[key] = TileCounts.Empty;
} }
#if isDemo #if isDemo
_simulationFlushTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs), DispatcherPriority.Background, SimulationFlushTimer_Tick, _dispatcher) _simulationFlushTimer = _dispatcher.CreateTimer(TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs), SimulationFlushTimer_Tick);
{
IsEnabled = false
};
#endif #endif
} }
static TicketOverviewUpdateService() private static readonly Lazy<TicketOverviewUpdateService> _instance =
{ new Lazy<TicketOverviewUpdateService>(() => new TicketOverviewUpdateService());
Instance = new TicketOverviewUpdateService();
}
public static TicketOverviewUpdateService Instance { get; } public static TicketOverviewUpdateService Instance => _instance.Value;
#endregion
#region Public API
public event EventHandler<TicketOverviewCountsChangedEventArgs> OverviewCountsChanged; public event EventHandler<TicketOverviewCountsChangedEventArgs> OverviewCountsChanged;
@@ -113,6 +139,10 @@ namespace FasdDesktopUi.Basics.Services
UpdateAvailability(false); UpdateAvailability(false);
} }
#endregion
#region Lifecycle
public void UpdateAvailability(bool isEnabled) public void UpdateAvailability(bool isEnabled)
{ {
if (isEnabled) if (isEnabled)
@@ -146,7 +176,7 @@ namespace FasdDesktopUi.Basics.Services
_isDemo = true; _isDemo = true;
LoadPersistedDemoTickets(); LoadPersistedDemoTickets();
#else #else
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true; _isDemo = _communicationSource.Resolve()?.IsDemo() == true;
#endif #endif
InitializeTimers(); InitializeTimers();
@@ -171,7 +201,7 @@ namespace FasdDesktopUi.Basics.Services
_retryScopes.Clear(); _retryScopes.Clear();
} }
_dispatcher.InvokeAsync(() => _ = _dispatcher.InvokeAsync(() =>
{ {
_personalTimer?.Stop(); _personalTimer?.Stop();
_roleTimer?.Stop(); _roleTimer?.Stop();
@@ -185,6 +215,10 @@ namespace FasdDesktopUi.Basics.Services
}); });
} }
#endregion
#region Polling and Fetch Pipeline
private void InitializeTimers() private void InitializeTimers()
{ {
_personalTimer = CreateScopeTimer(TileScope.Personal); _personalTimer = CreateScopeTimer(TileScope.Personal);
@@ -194,33 +228,15 @@ namespace FasdDesktopUi.Basics.Services
_roleTimer?.Start(); _roleTimer?.Start();
} }
private DispatcherTimer CreateScopeTimer(TileScope scope) private ITicketOverviewTimer CreateScopeTimer(TileScope scope)
{ {
var interval = GetPollingInterval(scope); var interval = GetPollingInterval(scope);
var timer = new DispatcherTimer(interval, DispatcherPriority.Background, async (s, e) => await FetchAsync(scope).ConfigureAwait(false), _dispatcher) return _dispatcher.CreateTimer(interval, () => _ = FetchAsync(scope));
{
IsEnabled = false
};
return timer;
} }
private TimeSpan GetPollingInterval(TileScope scope) private TimeSpan GetPollingInterval(TileScope scope)
{ {
var ticketConfig = cFasdCockpitConfig.Instance?.Global?.TicketConfiguration; int minutes = _settingsProvider.GetPollingMinutes(scope);
int minutes = scope == TileScope.Role
? cF4sdTicketConfig.DefaultOverviewPollingRole
: cF4sdTicketConfig.DefaultOverviewPollingPersonal;
if (ticketConfig != null)
{
minutes = scope == TileScope.Role
? ticketConfig.OverviewPollingRole
: ticketConfig.OverviewPollingPersonal;
}
if (minutes < 1)
minutes = 1;
return TimeSpan.FromMinutes(minutes); return TimeSpan.FromMinutes(minutes);
} }
@@ -291,7 +307,7 @@ namespace FasdDesktopUi.Basics.Services
if (!_isEnabled) if (!_isEnabled)
return; return;
var communication = cFasdCockpitCommunicationBase.Instance; var communication = _communicationSource.Resolve();
if (communication == null) if (communication == null)
{ {
ScheduleFetchRetry(scope); ScheduleFetchRetry(scope);
@@ -328,9 +344,13 @@ namespace FasdDesktopUi.Basics.Services
} }
} }
#endregion
#region Count Processing
private void RefreshTimerIntervals() private void RefreshTimerIntervals()
{ {
_dispatcher.InvokeAsync(() => _ = _dispatcher.InvokeAsync(() =>
{ {
if (_personalTimer != null) if (_personalTimer != null)
_personalTimer.Interval = GetPollingInterval(TileScope.Personal); _personalTimer.Interval = GetPollingInterval(TileScope.Personal);
@@ -345,39 +365,26 @@ namespace FasdDesktopUi.Basics.Services
if (newCounts == null) if (newCounts == null)
return; return;
var hasInitializedScope = _initializedScopes.Contains(scope); bool hasInitializedScope;
var changes = new List<TileCountChange>(); lock (_fetchLock)
foreach (var key in OverviewKeys)
{ {
var previous = _currentCounts.TryGetValue(key, out var counts) ? counts : TileCounts.Empty; hasInitializedScope = _initializedScopes.Contains(scope);
var incoming = newCounts.TryGetValue(key, out var value) ? value : 0;
TileCounts updated;
int oldValue;
if (scope == TileScope.Role)
{
updated = new TileCounts(previous.Personal, incoming);
oldValue = previous.Role;
}
else
{
updated = new TileCounts(incoming, previous.Role);
oldValue = previous.Personal;
} }
_currentCounts[key] = updated; var result = TicketOverviewCountProcessor.Calculate(_currentCounts, OverviewKeys, scope, newCounts, hasInitializedScope);
_currentCounts.Clear();
if (hasInitializedScope && oldValue != incoming) foreach (var kvp in result.UpdatedCounts)
{ {
changes.Add(new TileCountChange(key, scope, oldValue, incoming)); _currentCounts[kvp.Key] = kvp.Value;
}
} }
if (!hasInitializedScope) if (result.IsInitialization)
{
lock (_fetchLock)
{ {
_initializedScopes.Add(scope); _initializedScopes.Add(scope);
}
var initArgs = new TicketOverviewCountsChangedEventArgs( var initArgs = new TicketOverviewCountsChangedEventArgs(
Array.Empty<TileCountChange>(), Array.Empty<TileCountChange>(),
new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase), new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase),
@@ -386,16 +393,20 @@ namespace FasdDesktopUi.Basics.Services
return; return;
} }
if (changes.Count == 0) if (result.Changes.Count == 0)
return; return;
var args = new TicketOverviewCountsChangedEventArgs(changes, new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase)); var args = new TicketOverviewCountsChangedEventArgs(result.Changes, new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase));
OverviewCountsChanged?.Invoke(this, args); OverviewCountsChanged?.Invoke(this, args);
} }
#endregion
#region Demo - Public
public void SimulateDemoTicket() public void SimulateDemoTicket()
{ {
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true; _isDemo = _communicationSource.Resolve()?.IsDemo() == true;
if (!_isDemo) if (!_isDemo)
return; return;
@@ -407,7 +418,7 @@ namespace FasdDesktopUi.Basics.Services
return; return;
} }
_pendingSimulations++; Interlocked.Increment(ref _pendingSimulations);
if (_simulationFlushTimer != null) if (_simulationFlushTimer != null)
{ {
@@ -417,8 +428,8 @@ namespace FasdDesktopUi.Basics.Services
} }
else else
{ {
ProcessDemoSimulations(_pendingSimulations); var count = Interlocked.Exchange(ref _pendingSimulations, 0);
_pendingSimulations = 0; ProcessDemoSimulations(count);
} }
#endif #endif
} }
@@ -437,7 +448,11 @@ namespace FasdDesktopUi.Basics.Services
return Enumerable.Empty<cF4sdApiSearchResultRelation>(); return Enumerable.Empty<cF4sdApiSearchResultRelation>();
} }
#endregion
#if isDemo #if isDemo
#region Demo - Internals
private void LoadPersistedDemoTickets() private void LoadPersistedDemoTickets()
{ {
var data = TicketOverviewDataStore.LoadData(); var data = TicketOverviewDataStore.LoadData();
@@ -471,11 +486,10 @@ namespace FasdDesktopUi.Basics.Services
_demoTemplates.AddRange(templates); _demoTemplates.AddRange(templates);
} }
private void SimulationFlushTimer_Tick(object sender, EventArgs e) private void SimulationFlushTimer_Tick()
{ {
_simulationFlushTimer.Stop(); _simulationFlushTimer.Stop();
var count = _pendingSimulations; var count = Interlocked.Exchange(ref _pendingSimulations, 0);
_pendingSimulations = 0;
ProcessDemoSimulations(count); ProcessDemoSimulations(count);
} }
@@ -511,7 +525,9 @@ namespace FasdDesktopUi.Basics.Services
if (appliedChanges.Count == 0) if (appliedChanges.Count == 0)
return; return;
var args = new TicketOverviewCountsChangedEventArgs(appliedChanges, new Dictionary<string, TileCounts>(_currentCounts)); var args = new TicketOverviewCountsChangedEventArgs(
appliedChanges,
new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase));
OverviewCountsChanged?.Invoke(this, args); OverviewCountsChanged?.Invoke(this, args);
} }
@@ -611,10 +627,7 @@ namespace FasdDesktopUi.Basics.Services
if (!string.IsNullOrWhiteSpace(record.Summary)) if (!string.IsNullOrWhiteSpace(record.Summary))
_usedSummaries.Add(record.Summary); _usedSummaries.Add(record.Summary);
if (cFasdCockpitCommunicationBase.Instance is cFasdCockpitCommunicationDemo demoCommunication) _communicationSource.Resolve()?.RegisterGeneratedTicket(record);
{
demoCommunication.RegisterGeneratedTicket(record);
}
if (!_currentCounts.TryGetValue(record.TileKey, out var previousCounts)) if (!_currentCounts.TryGetValue(record.TileKey, out var previousCounts))
previousCounts = TileCounts.Empty; previousCounts = TileCounts.Empty;
@@ -761,22 +774,19 @@ namespace FasdDesktopUi.Basics.Services
}; };
} }
#endregion
#endif #endif
#region Utilities
public Dictionary<string, int> GetCountsForScope(bool useRoleScope) public Dictionary<string, int> GetCountsForScope(bool useRoleScope)
{ {
return _currentCounts.ToDictionary(kvp => kvp.Key, kvp => useRoleScope ? kvp.Value.Role : kvp.Value.Personal, StringComparer.OrdinalIgnoreCase); return _currentCounts.ToDictionary(kvp => kvp.Key, kvp => useRoleScope ? kvp.Value.Role : kvp.Value.Personal, StringComparer.OrdinalIgnoreCase);
} }
private int GetDemoRelationCount(string key, bool useRoleScope) #endregion
{
lock (_demoRelations) #region Retry Handling
{
if (_demoRelations.TryGetValue((key, useRoleScope), out var list))
return list.Count;
}
return 0;
}
private void ScheduleFetchRetry(TileScope scope) private void ScheduleFetchRetry(TileScope scope)
{ {
@@ -814,7 +824,9 @@ namespace FasdDesktopUi.Basics.Services
{ {
System.Diagnostics.Debug.WriteLine($"[TicketOverview] Retry scheduling failed: {ex}"); System.Diagnostics.Debug.WriteLine($"[TicketOverview] Retry scheduling failed: {ex}");
} }
}, DispatcherPriority.Background); });
} }
#endregion
} }
} }

View File

@@ -225,6 +225,10 @@
<Compile Include="Basics\Helper\UiElementHelper.cs" /> <Compile Include="Basics\Helper\UiElementHelper.cs" />
<Compile Include="Basics\HotKeyManager.cs" /> <Compile Include="Basics\HotKeyManager.cs" />
<Compile Include="Basics\Helper\TrayTicketNotificationManager.cs" /> <Compile Include="Basics\Helper\TrayTicketNotificationManager.cs" />
<Compile Include="Basics\Services\Models\TicketOverviewCommunicationSource.cs" />
<Compile Include="Basics\Services\Models\TicketOverviewDispatcher.cs" />
<Compile Include="Basics\Services\Models\TicketOverviewCountProcessor.cs" />
<Compile Include="Basics\Services\Models\TicketOverviewSettingsProvider.cs" />
<Compile Include="Basics\Services\Models\TicketOverviewCountsChangedEventArgs.cs" /> <Compile Include="Basics\Services\Models\TicketOverviewCountsChangedEventArgs.cs" />
<Compile Include="Basics\Services\TicketOverviewUpdateService.cs" /> <Compile Include="Basics\Services\TicketOverviewUpdateService.cs" />
<Compile Include="Basics\Models\ConnectionStatusHelper.cs" /> <Compile Include="Basics\Models\ConnectionStatusHelper.cs" />