Testing und interfaces
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
using C4IT.FASD.Base;
|
||||
using C4IT.FASD.Cockpit.Communication;
|
||||
using FasdDesktopUi;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using C4IT.FASD.Base;
|
||||
using C4IT.FASD.Cockpit.Communication;
|
||||
using FasdDesktopUi.Basics.Models;
|
||||
using FasdDesktopUi.Basics.Services.Models;
|
||||
#if isDemo
|
||||
@@ -18,8 +17,11 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace FasdDesktopUi.Basics.Services
|
||||
{
|
||||
public sealed class TicketOverviewUpdateService
|
||||
{
|
||||
public sealed class TicketOverviewUpdateService
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly ITicketOverviewCommunicationSource _communicationSource;
|
||||
private static readonly string[] OverviewKeys = new[]
|
||||
{
|
||||
"TicketsNew",
|
||||
@@ -33,54 +35,78 @@ namespace FasdDesktopUi.Basics.Services
|
||||
"UnassignedTickets",
|
||||
"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 Key, bool UseRoleScope), List<cF4sdApiSearchResultRelation>> _demoRelations = new Dictionary<(string, bool), List<cF4sdApiSearchResultRelation>>();
|
||||
private readonly HashSet<TileScope> _pendingScopes = new HashSet<TileScope>();
|
||||
private readonly HashSet<TileScope> _initializedScopes = new HashSet<TileScope>();
|
||||
private readonly object _fetchLock = new object();
|
||||
private readonly HashSet<TileScope> _retryScopes = new HashSet<TileScope>();
|
||||
private DispatcherTimer _personalTimer;
|
||||
private DispatcherTimer _roleTimer;
|
||||
private ITicketOverviewTimer _personalTimer;
|
||||
private ITicketOverviewTimer _roleTimer;
|
||||
private Task _fetchWorker;
|
||||
private bool _retryScheduled;
|
||||
private bool _isDemo;
|
||||
private bool _initialized;
|
||||
private bool _isEnabled;
|
||||
private readonly Random _random = new Random();
|
||||
#if isDemo
|
||||
private readonly List<DemoTicketRecord> _persistedDemoTickets = new List<DemoTicketRecord>();
|
||||
private readonly List<DemoTicketTemplate> _demoTemplates = new List<DemoTicketTemplate>();
|
||||
private readonly HashSet<string> _usedSummaries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
private const int SimulationHotkeyDelayMs = 400;
|
||||
private int _pendingSimulations;
|
||||
private DispatcherTimer _simulationFlushTimer;
|
||||
#endif
|
||||
|
||||
private TicketOverviewUpdateService()
|
||||
{
|
||||
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
foreach (var key in OverviewKeys)
|
||||
{
|
||||
_currentCounts[key] = TileCounts.Empty;
|
||||
}
|
||||
#if isDemo
|
||||
_simulationFlushTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs), DispatcherPriority.Background, SimulationFlushTimer_Tick, _dispatcher)
|
||||
{
|
||||
IsEnabled = false
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
static TicketOverviewUpdateService()
|
||||
#if isDemo
|
||||
private readonly List<DemoTicketRecord> _persistedDemoTickets = new List<DemoTicketRecord>();
|
||||
private readonly List<DemoTicketTemplate> _demoTemplates = new List<DemoTicketTemplate>();
|
||||
private readonly HashSet<string> _usedSummaries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
private const int SimulationHotkeyDelayMs = 400;
|
||||
private int _pendingSimulations;
|
||||
private ITicketOverviewTimer _simulationFlushTimer;
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
#region Construction and Singleton
|
||||
|
||||
private TicketOverviewUpdateService()
|
||||
: this(
|
||||
new TicketOverviewCommunicationSource(),
|
||||
new TicketOverviewDispatcher(Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher),
|
||||
new TicketOverviewSettingsProvider())
|
||||
{
|
||||
Instance = new TicketOverviewUpdateService();
|
||||
}
|
||||
|
||||
public static TicketOverviewUpdateService Instance { get; }
|
||||
|
||||
public event EventHandler<TicketOverviewCountsChangedEventArgs> OverviewCountsChanged;
|
||||
|
||||
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)
|
||||
{
|
||||
_currentCounts[key] = TileCounts.Empty;
|
||||
}
|
||||
#if isDemo
|
||||
_simulationFlushTimer = _dispatcher.CreateTimer(TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs), SimulationFlushTimer_Tick);
|
||||
#endif
|
||||
}
|
||||
|
||||
private static readonly Lazy<TicketOverviewUpdateService> _instance =
|
||||
new Lazy<TicketOverviewUpdateService>(() => new TicketOverviewUpdateService());
|
||||
|
||||
public static TicketOverviewUpdateService Instance => _instance.Value;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public API
|
||||
|
||||
public event EventHandler<TicketOverviewCountsChangedEventArgs> OverviewCountsChanged;
|
||||
|
||||
public IReadOnlyDictionary<string, TileCounts> CurrentCounts => _currentCounts;
|
||||
|
||||
public bool IsScopeInitialized(TileScope scope)
|
||||
@@ -113,6 +139,10 @@ namespace FasdDesktopUi.Basics.Services
|
||||
UpdateAvailability(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
public void UpdateAvailability(bool isEnabled)
|
||||
{
|
||||
if (isEnabled)
|
||||
@@ -146,7 +176,7 @@ namespace FasdDesktopUi.Basics.Services
|
||||
_isDemo = true;
|
||||
LoadPersistedDemoTickets();
|
||||
#else
|
||||
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true;
|
||||
_isDemo = _communicationSource.Resolve()?.IsDemo() == true;
|
||||
#endif
|
||||
|
||||
InitializeTimers();
|
||||
@@ -171,7 +201,7 @@ namespace FasdDesktopUi.Basics.Services
|
||||
_retryScopes.Clear();
|
||||
}
|
||||
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
_ = _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_personalTimer?.Stop();
|
||||
_roleTimer?.Stop();
|
||||
@@ -185,6 +215,10 @@ namespace FasdDesktopUi.Basics.Services
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Polling and Fetch Pipeline
|
||||
|
||||
private void InitializeTimers()
|
||||
{
|
||||
_personalTimer = CreateScopeTimer(TileScope.Personal);
|
||||
@@ -194,33 +228,15 @@ namespace FasdDesktopUi.Basics.Services
|
||||
_roleTimer?.Start();
|
||||
}
|
||||
|
||||
private DispatcherTimer CreateScopeTimer(TileScope scope)
|
||||
private ITicketOverviewTimer CreateScopeTimer(TileScope scope)
|
||||
{
|
||||
var interval = GetPollingInterval(scope);
|
||||
var timer = new DispatcherTimer(interval, DispatcherPriority.Background, async (s, e) => await FetchAsync(scope).ConfigureAwait(false), _dispatcher)
|
||||
{
|
||||
IsEnabled = false
|
||||
};
|
||||
return timer;
|
||||
return _dispatcher.CreateTimer(interval, () => _ = FetchAsync(scope));
|
||||
}
|
||||
|
||||
private TimeSpan GetPollingInterval(TileScope scope)
|
||||
{
|
||||
var ticketConfig = cFasdCockpitConfig.Instance?.Global?.TicketConfiguration;
|
||||
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;
|
||||
|
||||
int minutes = _settingsProvider.GetPollingMinutes(scope);
|
||||
return TimeSpan.FromMinutes(minutes);
|
||||
}
|
||||
|
||||
@@ -291,7 +307,7 @@ namespace FasdDesktopUi.Basics.Services
|
||||
if (!_isEnabled)
|
||||
return;
|
||||
|
||||
var communication = cFasdCockpitCommunicationBase.Instance;
|
||||
var communication = _communicationSource.Resolve();
|
||||
if (communication == null)
|
||||
{
|
||||
ScheduleFetchRetry(scope);
|
||||
@@ -328,9 +344,13 @@ namespace FasdDesktopUi.Basics.Services
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Count Processing
|
||||
|
||||
private void RefreshTimerIntervals()
|
||||
{
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
_ = _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (_personalTimer != null)
|
||||
_personalTimer.Interval = GetPollingInterval(TileScope.Personal);
|
||||
@@ -345,39 +365,26 @@ namespace FasdDesktopUi.Basics.Services
|
||||
if (newCounts == null)
|
||||
return;
|
||||
|
||||
var hasInitializedScope = _initializedScopes.Contains(scope);
|
||||
var changes = new List<TileCountChange>();
|
||||
|
||||
foreach (var key in OverviewKeys)
|
||||
bool hasInitializedScope;
|
||||
lock (_fetchLock)
|
||||
{
|
||||
var previous = _currentCounts.TryGetValue(key, out var counts) ? counts : TileCounts.Empty;
|
||||
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;
|
||||
|
||||
if (hasInitializedScope && oldValue != incoming)
|
||||
{
|
||||
changes.Add(new TileCountChange(key, scope, oldValue, incoming));
|
||||
}
|
||||
hasInitializedScope = _initializedScopes.Contains(scope);
|
||||
}
|
||||
|
||||
if (!hasInitializedScope)
|
||||
var result = TicketOverviewCountProcessor.Calculate(_currentCounts, OverviewKeys, scope, newCounts, hasInitializedScope);
|
||||
_currentCounts.Clear();
|
||||
foreach (var kvp in result.UpdatedCounts)
|
||||
{
|
||||
_initializedScopes.Add(scope);
|
||||
_currentCounts[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
if (result.IsInitialization)
|
||||
{
|
||||
lock (_fetchLock)
|
||||
{
|
||||
_initializedScopes.Add(scope);
|
||||
}
|
||||
|
||||
var initArgs = new TicketOverviewCountsChangedEventArgs(
|
||||
Array.Empty<TileCountChange>(),
|
||||
new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase),
|
||||
@@ -386,18 +393,22 @@ namespace FasdDesktopUi.Basics.Services
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.Count == 0)
|
||||
if (result.Changes.Count == 0)
|
||||
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);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Demo - Public
|
||||
|
||||
public void SimulateDemoTicket()
|
||||
{
|
||||
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true;
|
||||
if (!_isDemo)
|
||||
return;
|
||||
public void SimulateDemoTicket()
|
||||
{
|
||||
_isDemo = _communicationSource.Resolve()?.IsDemo() == true;
|
||||
if (!_isDemo)
|
||||
return;
|
||||
|
||||
#if isDemo
|
||||
if (_demoTemplates.Count == 0)
|
||||
@@ -407,40 +418,44 @@ namespace FasdDesktopUi.Basics.Services
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingSimulations++;
|
||||
|
||||
if (_simulationFlushTimer != null)
|
||||
{
|
||||
Interlocked.Increment(ref _pendingSimulations);
|
||||
|
||||
if (_simulationFlushTimer != null)
|
||||
{
|
||||
_simulationFlushTimer.Stop();
|
||||
_simulationFlushTimer.Interval = TimeSpan.FromMilliseconds(SimulationHotkeyDelayMs);
|
||||
_simulationFlushTimer.Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessDemoSimulations(_pendingSimulations);
|
||||
_pendingSimulations = 0;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var count = Interlocked.Exchange(ref _pendingSimulations, 0);
|
||||
ProcessDemoSimulations(count);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public IEnumerable<cF4sdApiSearchResultRelation> GetDemoRelations(string key, bool useRoleScope)
|
||||
{
|
||||
if (!_isDemo)
|
||||
return Enumerable.Empty<cF4sdApiSearchResultRelation>();
|
||||
public IEnumerable<cF4sdApiSearchResultRelation> GetDemoRelations(string key, bool useRoleScope)
|
||||
{
|
||||
if (!_isDemo)
|
||||
return Enumerable.Empty<cF4sdApiSearchResultRelation>();
|
||||
|
||||
lock (_demoRelations)
|
||||
{
|
||||
if (_demoRelations.TryGetValue((key, useRoleScope), out var list))
|
||||
return list.ToList();
|
||||
}
|
||||
|
||||
return Enumerable.Empty<cF4sdApiSearchResultRelation>();
|
||||
}
|
||||
|
||||
#if isDemo
|
||||
private void LoadPersistedDemoTickets()
|
||||
{
|
||||
var data = TicketOverviewDataStore.LoadData();
|
||||
|
||||
return Enumerable.Empty<cF4sdApiSearchResultRelation>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#if isDemo
|
||||
#region Demo - Internals
|
||||
|
||||
private void LoadPersistedDemoTickets()
|
||||
{
|
||||
var data = TicketOverviewDataStore.LoadData();
|
||||
|
||||
_demoTemplates.Clear();
|
||||
if (data.Templates != null)
|
||||
@@ -471,13 +486,12 @@ namespace FasdDesktopUi.Basics.Services
|
||||
_demoTemplates.AddRange(templates);
|
||||
}
|
||||
|
||||
private void SimulationFlushTimer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
_simulationFlushTimer.Stop();
|
||||
var count = _pendingSimulations;
|
||||
_pendingSimulations = 0;
|
||||
ProcessDemoSimulations(count);
|
||||
}
|
||||
private void SimulationFlushTimer_Tick()
|
||||
{
|
||||
_simulationFlushTimer.Stop();
|
||||
var count = Interlocked.Exchange(ref _pendingSimulations, 0);
|
||||
ProcessDemoSimulations(count);
|
||||
}
|
||||
|
||||
private void ProcessDemoSimulations(int count)
|
||||
{
|
||||
@@ -511,7 +525,9 @@ namespace FasdDesktopUi.Basics.Services
|
||||
if (appliedChanges.Count == 0)
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -611,10 +627,7 @@ namespace FasdDesktopUi.Basics.Services
|
||||
if (!string.IsNullOrWhiteSpace(record.Summary))
|
||||
_usedSummaries.Add(record.Summary);
|
||||
|
||||
if (cFasdCockpitCommunicationBase.Instance is cFasdCockpitCommunicationDemo demoCommunication)
|
||||
{
|
||||
demoCommunication.RegisterGeneratedTicket(record);
|
||||
}
|
||||
_communicationSource.Resolve()?.RegisterGeneratedTicket(record);
|
||||
|
||||
if (!_currentCounts.TryGetValue(record.TileKey, out var previousCounts))
|
||||
previousCounts = TileCounts.Empty;
|
||||
@@ -759,25 +772,22 @@ namespace FasdDesktopUi.Basics.Services
|
||||
CreatedAt = createdAt,
|
||||
Detail = detail
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
public Dictionary<string, int> GetCountsForScope(bool useRoleScope)
|
||||
{
|
||||
return _currentCounts.ToDictionary(kvp => kvp.Key, kvp => useRoleScope ? kvp.Value.Role : kvp.Value.Personal, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private int GetDemoRelationCount(string key, bool useRoleScope)
|
||||
{
|
||||
lock (_demoRelations)
|
||||
{
|
||||
if (_demoRelations.TryGetValue((key, useRoleScope), out var list))
|
||||
return list.Count;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endif
|
||||
|
||||
#region Utilities
|
||||
|
||||
public Dictionary<string, int> GetCountsForScope(bool useRoleScope)
|
||||
{
|
||||
return _currentCounts.ToDictionary(kvp => kvp.Key, kvp => useRoleScope ? kvp.Value.Role : kvp.Value.Personal, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Retry Handling
|
||||
|
||||
private void ScheduleFetchRetry(TileScope scope)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
@@ -814,7 +824,9 @@ namespace FasdDesktopUi.Basics.Services
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TicketOverview] Retry scheduling failed: {ex}");
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user