Fixed the warning about unsaved changes appearing only by chance (#1050)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-11 16:20:57 +02:00
1 parent 468d28aa94
commit c43b52d483
11 files changed
+213 -20

No files matched your search

@@ -6,5 +6,9 @@ public interface IMessageBusReceiver
{
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
/// <summary>
/// Answers a question which was asked through MessageBus.SendMessageUseFirstResult.
/// </summary>
/// <returns>The answer, or MessageBusAnswer.NONE when this receiver does not handle the question.</returns>
public Task<MessageBusAnswer<TResult>> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
}
+26 -3
View File
@@ -208,19 +208,42 @@ public sealed class MessageBus
return messages;
}
/// <summary>
/// Asks the receivers a question and returns the first answer.
/// </summary>
/// <remarks>
/// Every receiver which never narrowed down its events gets the question as well, but only the ones
/// which handle it answer. All others return MessageBusAnswer.NONE and are passed over, cf. the remarks
/// on MessageBusAnswer for why a bare result cannot express that.
/// <br/><br/>
/// A question stays within the circuit, i.e., the window, of the sender. The app keeps the circuits of
/// reloaded windows around for a long time, cf. the retention settings in Program.cs, and their components
/// are still registered: the chat of such an old window must not answer for the one the user is looking
/// at. Receivers without a circuit, such as hosted services, serve every window, and a sender without a
/// circuit asks all receivers.
/// </remarks>
/// <param name="sendingComponent">That's you, the sender.</param>
/// <param name="triggeredEvent">The question to ask.</param>
/// <param name="data">What the question needs to be answered, if anything.</param>
/// <returns>The first answer, or the default of TResult when no receiver answers. Asking for unsaved
/// changes thus yields false when no chat is open.</returns>
public async Task<TResult?> SendMessageUseFirstResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data = default)
{
var senderCircuit = sendingComponent is IMessageBusReceiver sender && this.receiverCircuits.TryGetValue(sender, out var circuitState) ? circuitState : null;
foreach (var (receiver, componentFilter) in this.componentFilters)
{
if (componentFilter.Length > 0 && sendingComponent is not null && !componentFilter.Contains(sendingComponent))
continue;
if (senderCircuit is not null && this.receiverCircuits.TryGetValue(receiver, out var receiverCircuit) && !ReferenceEquals(receiverCircuit, senderCircuit))
continue;
var eventFilter = this.componentEvents[receiver];
if (eventFilter.Length == 0 || eventFilter.Contains(triggeredEvent))
{
var result = await receiver.ProcessMessageWithResult<TPayload, TResult>(sendingComponent, triggeredEvent, data);
if (result is not null)
return (TResult) result;
var answer = await receiver.ProcessMessageWithResult<TPayload, TResult>(sendingComponent, triggeredEvent, data);
if (answer.IsAnswered)
return answer.Value;
}
}
@@ -0,0 +1,22 @@
namespace AIStudio.Tools;
/// <summary>
/// What one receiver says to a question on the message bus, cf. MessageBus.SendMessageUseFirstResult.
/// </summary>
/// <remarks>
/// A bare result cannot tell an answer apart from "this is not my question". Most receivers are components
/// which never narrow down their events, so every question reaches them, and the default they return looks
/// like an answer: for a bool, it is false. Such a false used to drown out the chat, the only receiver which
/// knows whether it has unsaved changes, and the warning about them appeared only by chance. That is why a
/// receiver states that it answers, and every other one returns NONE.
/// </remarks>
/// <param name="IsAnswered">True, when the receiver handles the question.</param>
/// <param name="Value">The answer. It means nothing unless IsAnswered is true.</param>
/// <typeparam name="TResult">The type of the answer.</typeparam>
public readonly record struct MessageBusAnswer<TResult>(bool IsAnswered, TResult? Value)
{
/// <summary>
/// The receiver does not handle this question.
/// </summary>
public static readonly MessageBusAnswer<TResult> NONE = new(false, default);
}
@@ -145,7 +145,7 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
}
}
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) => Task.FromResult<TResult?>(default);
public Task<MessageBusAnswer<TResult>> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) => Task.FromResult(MessageBusAnswer<TResult>.NONE);
#endregion
@@ -91,9 +91,9 @@ public sealed class RustAvailabilityMonitorService : BackgroundService, IMessage
return Task.CompletedTask;
}
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
public Task<MessageBusAnswer<TResult>> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
{
return Task.FromResult<TResult?>(default);
return Task.FromResult(MessageBusAnswer<TResult>.NONE);
}
private async Task VerifyRustAvailability()
@@ -81,9 +81,9 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
}
}
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
public Task<MessageBusAnswer<TResult>> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
{
return Task.FromResult<TResult?>(default);
return Task.FromResult(MessageBusAnswer<TResult>.NONE);
}
#endregion