Testing und interfaces
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,10 +221,14 @@
|
||||
<Compile Include="Basics\Helper\HealthCardDataHelper.cs" />
|
||||
<Compile Include="Basics\Helper\TicketDeepLinkHelper.cs" />
|
||||
<Compile Include="Basics\Helper\QuickActionProtocollHelper.cs" />
|
||||
<Compile Include="Basics\Helper\RichTextBoxHelper.cs" />
|
||||
<Compile Include="Basics\Helper\UiElementHelper.cs" />
|
||||
<Compile Include="Basics\HotKeyManager.cs" />
|
||||
<Compile Include="Basics\Helper\TrayTicketNotificationManager.cs" />
|
||||
<Compile Include="Basics\Helper\RichTextBoxHelper.cs" />
|
||||
<Compile Include="Basics\Helper\UiElementHelper.cs" />
|
||||
<Compile Include="Basics\HotKeyManager.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\TicketOverviewUpdateService.cs" />
|
||||
<Compile Include="Basics\Models\ConnectionStatusHelper.cs" />
|
||||
|
||||
Reference in New Issue
Block a user