Skip to content

Commit cdbe45b

Browse files
committed
Batch 5: Event system — thread safety, IUpdateEventListener, Progress events
- Rewrite EventManager with ConcurrentDictionary for thread safety Each handler invoked individually so one handler's exception doesn't prevent others from being called. - Add IUpdateEventListener interface for batch registration of all 7 event types at once via AddEventListener<T>() - Add UpdateEventListenerBase abstract class for optional overrides - Add ProgressEventArgs and AddListenerProgress to Bootstrap - Wire DownloadProgressReporter to fire Progress/Completed/Error events through EventManager automatically - Fix test to match new OnProgress signature Closes #369
1 parent 8d63132 commit cdbe45b

6 files changed

Lines changed: 143 additions & 69 deletions

File tree

src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,24 @@ public GeneralUpdateBootstrap AddListenerException(
329329

330330
public GeneralUpdateBootstrap AddListenerUpdateInfo(
331331
Action<object, UpdateInfoEventArgs> cb) => AddListener(cb);
332+
333+
public GeneralUpdateBootstrap AddListenerProgress(
334+
Action<object, ProgressEventArgs> cb) => AddListener(cb);
335+
336+
/// <summary>
337+
/// Batch-register an event listener implementing <see cref="IUpdateEventListener"/>.
338+
/// All 7 event handlers are registered at once.
339+
/// </summary>
340+
public GeneralUpdateBootstrap AddEventListener<TListener>() where TListener : IUpdateEventListener, new()
341+
{
342+
var listener = new TListener();
343+
AddListener<MultiAllDownloadCompletedEventArgs>((s, e) => listener.OnAllDownloadCompleted(e));
344+
AddListener<MultiDownloadCompletedEventArgs>((s, e) => listener.OnDownloadCompleted(e));
345+
AddListener<MultiDownloadErrorEventArgs>((s, e) => listener.OnDownloadError(e));
346+
AddListener<MultiDownloadStatisticsEventArgs>((s, e) => listener.OnDownloadStatistics(e));
347+
AddListener<UpdateInfoEventArgs>((s, e) => listener.OnUpdateInfo(e));
348+
AddListener<ExceptionEventArgs>((s, e) => listener.OnException(e));
349+
AddListener<ProgressEventArgs>((s, e) => listener.OnProgress(e));
350+
return this;
351+
}
332352
}
Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
using System;
2+
using System.Collections.Generic;
3+
using GeneralUpdate.Core.Download;
4+
using GeneralUpdate.Core.Event;
25
using IProgress = System.IProgress<GeneralUpdate.Core.Download.Models.DownloadProgress>;
36

47
namespace GeneralUpdate.Core.Download.Progress;
58

6-
/// <summary>Bridges IProgress to event-based callbacks for download status.</summary>
9+
/// <summary>Bridges IProgress to EventManager for backward-compatible event listeners.</summary>
710
public class DownloadProgressReporter : IProgress
811
{
9-
private readonly Action<Download.Models.DownloadProgress>? _onProgress;
12+
private readonly Action<Models.DownloadProgress>? _onProgress;
1013
private readonly Action? _onCompleted;
1114
private readonly Action? _onAllCompleted;
1215

1316
public DownloadProgressReporter(
14-
Action<Download.Models.DownloadProgress>? onProgress = null,
17+
Action<Models.DownloadProgress>? onProgress = null,
1518
Action? onCompleted = null,
1619
Action? onAllCompleted = null)
1720
{
@@ -20,14 +23,37 @@ public DownloadProgressReporter(
2023
_onAllCompleted = onAllCompleted;
2124
}
2225

23-
public void Report(Download.Models.DownloadProgress value)
26+
public void Report(Models.DownloadProgress value)
2427
{
2528
_onProgress?.Invoke(value);
26-
if (value.Status == Download.Models.DownloadStatus.Completed)
29+
30+
// Fire progress event via EventManager
31+
EventManager.Instance.Dispatch(this, new ProgressEventArgs(value));
32+
33+
if (value.Status == Models.DownloadStatus.Completed)
2734
{
2835
_onCompleted?.Invoke();
29-
if (value.Percentage >= 100)
30-
_onAllCompleted?.Invoke();
36+
EventManager.Instance.Dispatch(this,
37+
new MultiDownloadCompletedEventArgs(value.AssetName ?? "unknown", true));
38+
}
39+
40+
if (value.Status == Models.DownloadStatus.Failed)
41+
{
42+
EventManager.Instance.Dispatch(this,
43+
new MultiDownloadErrorEventArgs(new Exception("Download failed"), value.AssetName ?? "unknown"));
44+
}
45+
46+
if (value.Percentage >= 100)
47+
{
48+
_onAllCompleted?.Invoke();
49+
EventManager.Instance.Dispatch(this,
50+
new MultiAllDownloadCompletedEventArgs(true, new List<(object, string)>()));
3151
}
3252
}
53+
54+
/// <summary>
55+
/// Create an IProgress that dispatches progress events to EventManager.
56+
/// </summary>
57+
public static IProgress CreateEventBridge()
58+
=> new DownloadProgressReporter();
3359
}
Lines changed: 41 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,81 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
3-
using System.Diagnostics;
4+
using System.Threading;
45
using GeneralUpdate.Core;
56

67
namespace GeneralUpdate.Core.Event
78
{
9+
/// <summary>
10+
/// Thread-safe event manager using ConcurrentDictionary.
11+
/// Supports add/remove/dispatch without lock contention.
12+
/// </summary>
813
public class EventManager : IDisposable
914
{
1015
private static readonly Lazy<EventManager> _lazy = new(() => new EventManager());
11-
private Dictionary<Type, Delegate> _dicDelegates = new();
12-
private bool _disposed = false;
16+
private ConcurrentDictionary<Type, Delegate> _dicDelegates = new();
17+
private bool _disposed;
1318

1419
private EventManager() { }
1520

1621
public static EventManager Instance => _lazy.Value;
1722

1823
public void AddListener<TEventArgs>(Action<object, TEventArgs> listener) where TEventArgs : EventArgs
1924
{
20-
try
21-
{
22-
if (listener == null) throw new ArgumentNullException(nameof(listener));
23-
var delegateType = typeof(Action<object, TEventArgs>);
24-
if (_dicDelegates.ContainsKey(delegateType))
25-
{
26-
_dicDelegates[delegateType] = Delegate.Combine(_dicDelegates[delegateType], listener);
27-
}
28-
else
29-
{
30-
_dicDelegates.Add(delegateType, listener);
31-
}
32-
}
33-
catch (Exception e)
34-
{
35-
GeneralTracer.Error("The AddListener method in the EventManager class throws an exception.", e);
36-
}
25+
if (listener == null) throw new ArgumentNullException(nameof(listener));
26+
var type = typeof(Action<object, TEventArgs>);
27+
_dicDelegates.AddOrUpdate(type,
28+
_ => listener,
29+
(_, existing) => Delegate.Combine(existing, listener));
3730
}
3831

3932
public void RemoveListener<TEventArgs>(Action<object, TEventArgs> listener) where TEventArgs : EventArgs
4033
{
41-
try
34+
if (listener == null) throw new ArgumentNullException(nameof(listener));
35+
var type = typeof(Action<object, TEventArgs>);
36+
if (_dicDelegates.TryGetValue(type, out var existing))
4237
{
43-
if (listener == null) throw new ArgumentNullException(nameof(listener));
44-
var delegateType = typeof(Action<object, TEventArgs>);
45-
if (_dicDelegates.TryGetValue(delegateType, out var existingDelegate))
46-
{
47-
_dicDelegates[delegateType] = Delegate.Remove(existingDelegate, listener);
48-
}
49-
}
50-
catch (Exception e)
51-
{
52-
GeneralTracer.Error("The RemoveListener method in the EventManager class throws an exception.", e);
38+
var updated = Delegate.Remove(existing, listener);
39+
if (updated == null)
40+
_dicDelegates.TryRemove(type, out _);
41+
else
42+
_dicDelegates.TryUpdate(type, updated, existing);
5343
}
5444
}
5545

5646
public void Dispatch<TEventArgs>(object sender, TEventArgs eventArgs) where TEventArgs : EventArgs
5747
{
58-
try
48+
if (sender == null) throw new ArgumentNullException(nameof(sender));
49+
if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs));
50+
51+
var type = typeof(Action<object, TEventArgs>);
52+
if (_dicDelegates.TryGetValue(type, out var existingDelegate))
5953
{
60-
if (sender == null) throw new ArgumentNullException(nameof(sender));
61-
if (eventArgs == null) throw new ArgumentNullException(nameof(eventArgs));
62-
var delegateType = typeof(Action<object, TEventArgs>);
63-
if (_dicDelegates.TryGetValue(delegateType, out var existingDelegate))
54+
// Invoke each handler individually so one handler's exception
55+
// doesn't prevent others from being called.
56+
foreach (var handler in existingDelegate.GetInvocationList())
6457
{
65-
((Action<object, TEventArgs>)existingDelegate)?.Invoke(sender, eventArgs);
58+
try
59+
{
60+
((Action<object, TEventArgs>)handler).Invoke(sender, eventArgs);
61+
}
62+
catch (Exception e)
63+
{
64+
GeneralTracer.Error("EventManager.Dispatch handler threw an exception.", e);
65+
}
6666
}
6767
}
68-
catch (Exception e)
69-
{
70-
GeneralTracer.Error("The Dispatch method in the EventManager class throws an exception.", e);
71-
}
7268
}
7369

7470
public void Clear() => _dicDelegates.Clear();
7571

7672
public void Dispose()
7773
{
78-
try
79-
{
80-
if (!this._disposed)
81-
{
82-
_dicDelegates.Clear();
83-
_disposed = true;
84-
}
85-
}
86-
catch (Exception e)
74+
if (!_disposed)
8775
{
88-
GeneralTracer.Error("The Dispose method in the EventManager class throws an exception.", e);
76+
_dicDelegates.Clear();
77+
_disposed = true;
8978
}
9079
}
9180
}
92-
}
81+
}
Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,47 @@
1-
using System;
21
using GeneralUpdate.Core.Download;
3-
using GeneralUpdate.Core.Download.Models;
4-
using GeneralUpdate.Core.Event;
52

63
namespace GeneralUpdate.Core.Event;
74

8-
/// <summary>Batch event registration — implement once, register once.</summary>
5+
/// <summary>
6+
/// Batch registration interface for all update event types.
7+
/// Implement this interface and register via
8+
/// <c>new GeneralUpdateBootstrap().AddEventListener&lt;MyListener&gt;()</c>.
9+
/// </summary>
910
public interface IUpdateEventListener
1011
{
12+
/// <summary>All downloads completed.</summary>
1113
void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs args);
14+
15+
/// <summary>Single download completed.</summary>
1216
void OnDownloadCompleted(MultiDownloadCompletedEventArgs args);
17+
18+
/// <summary>Download error.</summary>
1319
void OnDownloadError(MultiDownloadErrorEventArgs args);
20+
21+
/// <summary>Download statistics updated.</summary>
1422
void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args);
23+
24+
/// <summary>Update information available.</summary>
1525
void OnUpdateInfo(UpdateInfoEventArgs args);
26+
27+
/// <summary>Exception occurred.</summary>
1628
void OnException(ExceptionEventArgs args);
17-
void OnProgress(DownloadProgress progress);
29+
30+
/// <summary>Real-time download progress.</summary>
31+
void OnProgress(ProgressEventArgs args);
1832
}
1933

20-
/// <summary>Progress event args for AddListenerProgress.</summary>
21-
public class ProgressEventArgs : EventArgs
34+
/// <summary>
35+
/// Base class that implements IUpdateEventListener with no-op methods.
36+
/// Inherit from this and override only the events you need.
37+
/// </summary>
38+
public abstract class UpdateEventListenerBase : IUpdateEventListener
2239
{
23-
public DownloadProgress Progress { get; }
24-
public ProgressEventArgs(DownloadProgress progress) => Progress = progress;
40+
public virtual void OnAllDownloadCompleted(MultiAllDownloadCompletedEventArgs args) { }
41+
public virtual void OnDownloadCompleted(MultiDownloadCompletedEventArgs args) { }
42+
public virtual void OnDownloadError(MultiDownloadErrorEventArgs args) { }
43+
public virtual void OnDownloadStatistics(MultiDownloadStatisticsEventArgs args) { }
44+
public virtual void OnUpdateInfo(UpdateInfoEventArgs args) { }
45+
public virtual void OnException(ExceptionEventArgs args) { }
46+
public virtual void OnProgress(ProgressEventArgs args) { }
2547
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using GeneralUpdate.Core.Download.Models;
3+
4+
namespace GeneralUpdate.Core.Event;
5+
6+
/// <summary>
7+
/// Progress event args — wraps a DownloadProgress snapshot.
8+
/// </summary>
9+
public class ProgressEventArgs : EventArgs
10+
{
11+
public DownloadProgress Progress { get; }
12+
13+
public ProgressEventArgs(DownloadProgress progress)
14+
{
15+
Progress = progress;
16+
}
17+
}

tests/CoreTest/Event/EventListenerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public void OnUpdateInfo(UpdateInfoEventArgs args)
3636
public void OnException(ExceptionEventArgs args)
3737
=> ExceptionCalls++;
3838

39-
public void OnProgress(DownloadProgress progress)
39+
public void OnProgress(ProgressEventArgs args)
4040
=> ProgressCalls++;
4141
}
4242

0 commit comments

Comments
 (0)