aktueller Stand
This commit is contained in:
@@ -7,8 +7,9 @@ using System.Windows.Threading;
|
||||
|
||||
using C4IT.FASD.Base;
|
||||
using C4IT.FASD.Cockpit.Communication;
|
||||
using FasdDesktopUi.Basics.Models;
|
||||
using FasdDesktopUi.Basics.Services.Models;
|
||||
using FasdDesktopUi;
|
||||
using FasdDesktopUi.Basics.Models;
|
||||
using FasdDesktopUi.Basics.Services.Models;
|
||||
#if isDemo
|
||||
using System.Net;
|
||||
using FasdCockpitCommunicationDemo;
|
||||
@@ -19,11 +20,10 @@ namespace FasdDesktopUi.Basics.Services
|
||||
{
|
||||
public sealed class TicketOverviewUpdateService
|
||||
{
|
||||
private static readonly TimeSpan RefreshInterval = TimeSpan.FromMinutes(5);
|
||||
private static readonly string[] OverviewKeys = new[]
|
||||
{
|
||||
"TicketsNew",
|
||||
"TicketsActive",
|
||||
private static readonly string[] OverviewKeys = new[]
|
||||
{
|
||||
"TicketsNew",
|
||||
"TicketsActive",
|
||||
"TicketsCritical",
|
||||
"TicketsNewInfo",
|
||||
"IncidentNew",
|
||||
@@ -33,16 +33,22 @@ namespace FasdDesktopUi.Basics.Services
|
||||
"UnassignedTickets",
|
||||
"UnassignedTicketsCritical"
|
||||
};
|
||||
private const string DemoTicketDetailsKey = "Demo.HasTicketDetails";
|
||||
private readonly Dispatcher _dispatcher;
|
||||
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 DispatcherTimer _timer;
|
||||
private bool _isFetching;
|
||||
private bool _fetchRetryPending;
|
||||
private bool _isDemo;
|
||||
private bool _initialized;
|
||||
private readonly Random _random = new Random();
|
||||
private const string DemoTicketDetailsKey = "Demo.HasTicketDetails";
|
||||
private readonly Dispatcher _dispatcher;
|
||||
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 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>();
|
||||
@@ -67,138 +73,338 @@ namespace FasdDesktopUi.Basics.Services
|
||||
#endif
|
||||
}
|
||||
|
||||
static TicketOverviewUpdateService()
|
||||
{
|
||||
#if isDemo
|
||||
Instance = new TicketOverviewUpdateService();
|
||||
#endif
|
||||
}
|
||||
|
||||
public static TicketOverviewUpdateService Instance { get; } = null;
|
||||
static TicketOverviewUpdateService()
|
||||
{
|
||||
Instance = new TicketOverviewUpdateService();
|
||||
}
|
||||
|
||||
public static TicketOverviewUpdateService Instance { get; }
|
||||
|
||||
public event EventHandler<TicketOverviewCountsChangedEventArgs> OverviewCountsChanged;
|
||||
|
||||
public IReadOnlyDictionary<string, TileCounts> CurrentCounts => _currentCounts;
|
||||
public IReadOnlyDictionary<string, TileCounts> CurrentCounts => _currentCounts;
|
||||
|
||||
public bool IsScopeInitialized(TileScope scope)
|
||||
{
|
||||
lock (_fetchLock)
|
||||
{
|
||||
return _initializedScopes.Contains(scope);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AreAllScopesInitialized
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_fetchLock)
|
||||
{
|
||||
return _initializedScopes.Contains(TileScope.Personal)
|
||||
&& _initializedScopes.Contains(TileScope.Role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
public void Start()
|
||||
{
|
||||
UpdateAvailability(true);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
UpdateAvailability(false);
|
||||
}
|
||||
|
||||
public void UpdateAvailability(bool isEnabled)
|
||||
{
|
||||
if (isEnabled)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
{
|
||||
_isEnabled = true;
|
||||
StartInternal();
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshTimerIntervals();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_isEnabled)
|
||||
StopInternal();
|
||||
|
||||
_isEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartInternal()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_initialized = true;
|
||||
#if isDemo
|
||||
_isDemo = true;
|
||||
LoadPersistedDemoTickets();
|
||||
#else
|
||||
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true;
|
||||
#endif
|
||||
|
||||
InitializeTimers();
|
||||
_ = FetchAsync();
|
||||
}
|
||||
|
||||
private void StopInternal()
|
||||
{
|
||||
if (!_initialized)
|
||||
return;
|
||||
|
||||
_initialized = false;
|
||||
lock (_fetchLock)
|
||||
{
|
||||
_pendingScopes.Clear();
|
||||
_initializedScopes.Clear();
|
||||
}
|
||||
|
||||
lock (_retryScopes)
|
||||
{
|
||||
_retryScheduled = false;
|
||||
_retryScopes.Clear();
|
||||
}
|
||||
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
_personalTimer?.Stop();
|
||||
_roleTimer?.Stop();
|
||||
_personalTimer = null;
|
||||
_roleTimer = null;
|
||||
|
||||
foreach (var key in OverviewKeys)
|
||||
{
|
||||
_currentCounts[key] = TileCounts.Empty;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeTimers()
|
||||
{
|
||||
_personalTimer = CreateScopeTimer(TileScope.Personal);
|
||||
_roleTimer = CreateScopeTimer(TileScope.Role);
|
||||
|
||||
_personalTimer?.Start();
|
||||
_roleTimer?.Start();
|
||||
}
|
||||
|
||||
private DispatcherTimer 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return TimeSpan.FromMinutes(minutes);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
#if isDemo
|
||||
_isDemo = true;
|
||||
LoadPersistedDemoTickets();
|
||||
#else
|
||||
_isDemo = cFasdCockpitCommunicationBase.Instance?.IsDemo() == true;
|
||||
#endif
|
||||
|
||||
if (!_isDemo)
|
||||
{
|
||||
_timer = new DispatcherTimer(RefreshInterval, DispatcherPriority.Background, async (s, e) => await FetchAsync().ConfigureAwait(false), _dispatcher);
|
||||
_timer.Start();
|
||||
_ = FetchAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = FetchAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task FetchAsync()
|
||||
{
|
||||
if (_isFetching)
|
||||
return;
|
||||
|
||||
var communication = cFasdCockpitCommunicationBase.Instance;
|
||||
if (communication == null)
|
||||
{
|
||||
ScheduleFetchRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
_isFetching = true;
|
||||
try
|
||||
{
|
||||
_isDemo = communication?.IsDemo() == true;
|
||||
if (_isDemo && _timer != null)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer = null;
|
||||
}
|
||||
var counts = await Task.Run(() => RetrieveCountsAsync()).ConfigureAwait(false);
|
||||
if (counts != null)
|
||||
{
|
||||
await _dispatcher.InvokeAsync(() => ProcessCounts(counts));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isFetching = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, TileCounts> RetrieveCountsAsync()
|
||||
{
|
||||
var communication = cFasdCockpitCommunicationBase.Instance;
|
||||
if (communication == null)
|
||||
return null;
|
||||
|
||||
var result = new Dictionary<string, TileCounts>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var key in OverviewKeys)
|
||||
{
|
||||
var personalTask = communication.GetTicketOverviewRelations(key, useRoleScope: false, count: 0);
|
||||
var roleTask = communication.GetTicketOverviewRelations(key, useRoleScope: true, count: 0);
|
||||
Task.WaitAll(personalTask, roleTask);
|
||||
|
||||
int personalCount = personalTask.Result?.Count ?? 0;
|
||||
int roleCount = roleTask.Result?.Count ?? 0;
|
||||
|
||||
if (_isDemo)
|
||||
{
|
||||
personalCount += GetDemoRelationCount(key, useRoleScope: false);
|
||||
roleCount += GetDemoRelationCount(key, useRoleScope: true);
|
||||
}
|
||||
|
||||
result[key] = new TileCounts(personalCount, roleCount);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ProcessCounts(Dictionary<string, TileCounts> newCounts)
|
||||
{
|
||||
var changes = new List<TileCountChange>();
|
||||
bool hasPrevious = _currentCounts.Values.Any(tc => tc.Personal > 0 || tc.Role > 0);
|
||||
|
||||
foreach (var key in OverviewKeys)
|
||||
{
|
||||
var previous = _currentCounts[key];
|
||||
var current = newCounts.TryGetValue(key, out var value) ? value : TileCounts.Empty;
|
||||
|
||||
if (previous.Personal != current.Personal)
|
||||
{
|
||||
changes.Add(new TileCountChange(key, TileScope.Personal, previous.Personal, current.Personal));
|
||||
}
|
||||
|
||||
if (previous.Role != current.Role)
|
||||
{
|
||||
changes.Add(new TileCountChange(key, TileScope.Role, previous.Role, current.Role));
|
||||
}
|
||||
|
||||
_currentCounts[key] = current;
|
||||
}
|
||||
|
||||
if (!hasPrevious)
|
||||
return;
|
||||
|
||||
if (changes.Count == 0)
|
||||
return;
|
||||
|
||||
var args = new TicketOverviewCountsChangedEventArgs(changes, new Dictionary<string, TileCounts>(_currentCounts));
|
||||
OverviewCountsChanged?.Invoke(this, args);
|
||||
}
|
||||
public Task FetchAsync()
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return QueueFetchAsync(new[] { TileScope.Personal, TileScope.Role });
|
||||
}
|
||||
|
||||
public Task FetchAsync(TileScope scope)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return QueueFetchAsync(new[] { scope });
|
||||
}
|
||||
|
||||
private Task QueueFetchAsync(IEnumerable<TileScope> scopes)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (scopes == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
lock (_fetchLock)
|
||||
{
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
_pendingScopes.Add(scope);
|
||||
}
|
||||
|
||||
if (_fetchWorker == null || _fetchWorker.IsCompleted)
|
||||
{
|
||||
_fetchWorker = Task.Run(ProcessFetchQueueAsync);
|
||||
}
|
||||
|
||||
return _fetchWorker;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessFetchQueueAsync()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
TileScope scope;
|
||||
|
||||
lock (_fetchLock)
|
||||
{
|
||||
if (_pendingScopes.Count == 0)
|
||||
{
|
||||
_fetchWorker = null;
|
||||
return;
|
||||
}
|
||||
|
||||
scope = _pendingScopes.First();
|
||||
_pendingScopes.Remove(scope);
|
||||
}
|
||||
|
||||
await FetchScopeAsync(scope).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchScopeAsync(TileScope scope)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return;
|
||||
|
||||
var communication = cFasdCockpitCommunicationBase.Instance;
|
||||
if (communication == null)
|
||||
{
|
||||
ScheduleFetchRetry(scope);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isDemo = communication.IsDemo();
|
||||
|
||||
var rawCounts = await communication.GetTicketOverviewCounts(OverviewKeys, scope == TileScope.Role).ConfigureAwait(false);
|
||||
var counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (rawCounts != null)
|
||||
{
|
||||
foreach (var kvp in rawCounts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(kvp.Key))
|
||||
continue;
|
||||
|
||||
counts[kvp.Key] = Math.Max(0, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (_isDemo)
|
||||
{
|
||||
foreach (var key in OverviewKeys)
|
||||
{
|
||||
var extras = GetDemoRelationCount(key, scope == TileScope.Role);
|
||||
if (counts.ContainsKey(key))
|
||||
counts[key] += extras;
|
||||
else
|
||||
counts[key] = extras;
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isEnabled)
|
||||
return;
|
||||
|
||||
await _dispatcher.InvokeAsync(() => ProcessScopeCounts(scope, counts));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TicketOverview] Fetch {scope} failed: {ex}");
|
||||
ScheduleFetchRetry(scope);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshTimerIntervals()
|
||||
{
|
||||
_dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (_personalTimer != null)
|
||||
_personalTimer.Interval = GetPollingInterval(TileScope.Personal);
|
||||
|
||||
if (_roleTimer != null)
|
||||
_roleTimer.Interval = GetPollingInterval(TileScope.Role);
|
||||
});
|
||||
}
|
||||
|
||||
private void ProcessScopeCounts(TileScope scope, IDictionary<string, int> newCounts)
|
||||
{
|
||||
if (newCounts == null)
|
||||
return;
|
||||
|
||||
var hasInitializedScope = _initializedScopes.Contains(scope);
|
||||
var changes = new List<TileCountChange>();
|
||||
|
||||
foreach (var key in OverviewKeys)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasInitializedScope)
|
||||
{
|
||||
_initializedScopes.Add(scope);
|
||||
var initArgs = new TicketOverviewCountsChangedEventArgs(
|
||||
Array.Empty<TileCountChange>(),
|
||||
new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase),
|
||||
scope);
|
||||
OverviewCountsChanged?.Invoke(this, initArgs);
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.Count == 0)
|
||||
return;
|
||||
|
||||
var args = new TicketOverviewCountsChangedEventArgs(changes, new Dictionary<string, TileCounts>(_currentCounts, StringComparer.OrdinalIgnoreCase));
|
||||
OverviewCountsChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
public void SimulateDemoTicket()
|
||||
{
|
||||
@@ -318,9 +524,9 @@ namespace FasdDesktopUi.Basics.Services
|
||||
if (appliedChanges.Count == 0)
|
||||
return;
|
||||
|
||||
var args = new TicketOverviewCountsChangedEventArgs(appliedChanges, new Dictionary<string, TileCounts>(_currentCounts));
|
||||
OverviewCountsChanged?.Invoke(this, args);
|
||||
}
|
||||
var args = new TicketOverviewCountsChangedEventArgs(appliedChanges, new Dictionary<string, TileCounts>(_currentCounts));
|
||||
OverviewCountsChanged?.Invoke(this, args);
|
||||
}
|
||||
|
||||
private void AddRelationForRecord(DemoTicketRecord record)
|
||||
{
|
||||
@@ -577,24 +783,43 @@ namespace FasdDesktopUi.Basics.Services
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void ScheduleFetchRetry()
|
||||
{
|
||||
if (_fetchRetryPending)
|
||||
return;
|
||||
|
||||
_fetchRetryPending = true;
|
||||
_ = _dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(250).ConfigureAwait(false);
|
||||
await FetchAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fetchRetryPending = false;
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
private void ScheduleFetchRetry(TileScope scope)
|
||||
{
|
||||
if (!_isEnabled)
|
||||
return;
|
||||
|
||||
lock (_retryScopes)
|
||||
{
|
||||
_retryScopes.Add(scope);
|
||||
if (_retryScheduled)
|
||||
return;
|
||||
|
||||
_retryScheduled = true;
|
||||
}
|
||||
|
||||
_ = _dispatcher.InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
|
||||
TileScope[] scopes;
|
||||
lock (_retryScopes)
|
||||
{
|
||||
scopes = _retryScopes.ToArray();
|
||||
_retryScopes.Clear();
|
||||
_retryScheduled = false;
|
||||
}
|
||||
|
||||
foreach (var pendingScope in scopes)
|
||||
{
|
||||
await FetchAsync(pendingScope).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[TicketOverview] Retry scheduling failed: {ex}");
|
||||
}
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user