From 205c2039f05327be47705e0ff4bd2d8d3de079b0 Mon Sep 17 00:00:00 2001 From: mark-sil <83427558+mark-sil@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:55:07 -0400 Subject: [PATCH] LT-22568: Part 1 - Scope Pub/Sub to a Main Window The Pub/Sub framework was allowing Subscribers to receive messages from all Publishers. In most cases we want this to be scoped to only include Subscribers with the same Main Window (IxWindow) as the Publisher. Now both the Publish() and Subscribe() methods take a IPubSubScope object as a Parameter (IxWindow is not available in the Pub/Sub dll). Note: We use the interface to prevent accidentally passing a dialog instead of the main window (ex. passing Form.ActiveForm inside a modal). --- .../Controls/DetailControls/DataTree.cs | 4 +- .../DetailControls/GhostStringSlice.cs | 4 +- Src/Common/Controls/XMLViews/BrowseViewer.cs | 2 +- .../Controls/XMLViews/XmlBrowseRDEView.cs | 4 +- .../Controls/XMLViews/XmlBrowseViewBase.cs | 6 +- .../Controls/XMLViews/XmlBrowseViewBaseVc.cs | 2 +- Src/Common/FieldWorks/FieldWorks.cs | 2 +- Src/Common/FwUtils/EndOfActionManager.cs | 32 ++- .../FwUtils/FwUtilsTests/PubSubSystemTests.cs | 216 ++++++++++++++++++ Src/Common/FwUtils/IPubSubScope.cs | 32 +++ Src/Common/FwUtils/ISubscriber.cs | 22 +- Src/Common/FwUtils/Publisher.cs | 40 ++-- .../FwUtils/PublisherParameterObject.cs | 10 +- Src/Common/FwUtils/Subscriber.cs | 46 ++-- Src/FdoUi/FdoUiCore.cs | 6 +- Src/LexText/Interlinear/ConcordanceControl.cs | 2 +- Src/LexText/Interlinear/InterlinMaster.cs | 6 +- .../InterlinearTextsRecordClerk.cs | 6 +- .../Interlinear/SandboxBase.ComboHandlers.cs | 2 +- Src/LexText/Interlinear/StatisticsView.cs | 2 +- .../DataNotebook/AnthroFieldMappingDlg.cs | 2 +- .../LexTextControls/EntryDlgListener.cs | 7 +- Src/LexText/LexTextControls/InsertEntryDlg.cs | 2 +- .../LexTextControls/LexImportWizardMarker.cs | 2 +- .../LexTextControls/RecordDlgListener.cs | 7 +- Src/LexText/LexTextDll/AreaListener.cs | 10 +- Src/LexText/Lexicon/FLExBridgeListener.cs | 6 +- Src/LexText/Lexicon/LexReferenceMultiSlice.cs | 2 +- .../Lexicon/LexReferenceSequenceView.cs | 2 +- Src/LexText/Lexicon/ReversalListener.cs | 2 +- .../Morphology/MasterCatDlgListener.cs | 7 +- Src/LexText/Morphology/MasterDlgListener.cs | 2 +- .../Morphology/MasterInflFeatDlgListener.cs | 7 +- .../Morphology/MasterPhonFeatDlgListener.cs | 7 +- .../Morphology/ParserAnalysisRemover.cs | 4 +- Src/LexText/Morphology/RespellerDlg.cs | 6 +- Src/LexText/Morphology/UserAnalysisRemover.cs | 2 +- Src/LexText/ParserUI/ImportWordSetDlg.cs | 6 +- Src/LexText/ParserUI/ParserListener.cs | 6 +- Src/XCore/xCoreInterfaces/IxCoreColleague.cs | 5 +- Src/XCore/xCoreInterfaces/PropertyTable.cs | 16 ++ Src/XCore/xWindow.cs | 20 +- Src/xWorks/CustomListDlg.cs | 2 +- Src/xWorks/DTMenuHandler.cs | 2 +- .../DictionaryConfigurationController.cs | 2 +- .../PreHistoricMigrator.cs | 2 +- Src/xWorks/ExportDialog.cs | 4 +- Src/xWorks/FwXApp.cs | 2 +- Src/xWorks/FwXWindow.cs | 10 +- Src/xWorks/GeneratedHtmlViewer.cs | 4 +- Src/xWorks/LinkListener.cs | 12 +- Src/xWorks/RecordBrowseView.cs | 4 +- Src/xWorks/RecordClerk.cs | 14 +- Src/xWorks/RecordDocView.cs | 2 +- Src/xWorks/RecordEditView.cs | 2 +- Src/xWorks/RecordView.cs | 2 +- Src/xWorks/XWorksViewBase.cs | 2 +- Src/xWorks/XhtmlDocView.cs | 4 +- Src/xWorks/XmlDocView.cs | 4 +- 59 files changed, 504 insertions(+), 146 deletions(-) create mode 100644 Src/Common/FwUtils/IPubSubScope.cs diff --git a/Src/Common/Controls/DetailControls/DataTree.cs b/Src/Common/Controls/DetailControls/DataTree.cs index 27ea2ea211..f83c79bea9 100644 --- a/Src/Common/Controls/DetailControls/DataTree.cs +++ b/Src/Common/Controls/DetailControls/DataTree.cs @@ -4404,8 +4404,8 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu if (PersistenceProvder != null) RestorePreferences(); - Subscriber.Subscribe(EventConstants.PostponePropChanged, PostponePropChanged); - Subscriber.Subscribe(EventConstants.JumpToField, JumpToField); + Subscriber.Subscribe(EventConstants.PostponePropChanged, PostponePropChanged, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.JumpToField, JumpToField, m_propertyTable.GetWindow()); } public IxCoreColleague[] GetMessageTargets() diff --git a/Src/Common/Controls/DetailControls/GhostStringSlice.cs b/Src/Common/Controls/DetailControls/GhostStringSlice.cs index a507cd4f07..73de88e2ed 100644 --- a/Src/Common/Controls/DetailControls/GhostStringSlice.cs +++ b/Src/Common/Controls/DetailControls/GhostStringSlice.cs @@ -459,12 +459,12 @@ private void SwitchToReal() int hvoNewObj; try { - Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, false)); + Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, false, datatree.PropTable?.GetWindow())); hvoNewObj = MakeRealObject(tssTyped); } finally { - Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, true)); + Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, true, datatree.PropTable?.GetWindow())); } // Now try to make a suitable selection in the slice that replaces this. diff --git a/Src/Common/Controls/XMLViews/BrowseViewer.cs b/Src/Common/Controls/XMLViews/BrowseViewer.cs index cae0c518cc..ee0171e387 100644 --- a/Src/Common/Controls/XMLViews/BrowseViewer.cs +++ b/Src/Common/Controls/XMLViews/BrowseViewer.cs @@ -3298,7 +3298,7 @@ public virtual void Init(Mediator mediator, PropertyTable propertyTable, XmlNode m_xbv.Init(mediator, propertyTable, configurationParameters); m_xbv.AccessibleName = "BrowseViewer"; m_mediator = mediator; - Subscriber.Subscribe(EventConstants.RemoveFilters, RemoveFilters); + Subscriber.Subscribe(EventConstants.RemoveFilters, RemoveFilters, propertyTable.GetWindow()); } /// /// Should not be called if disposed. diff --git a/Src/Common/Controls/XMLViews/XmlBrowseRDEView.cs b/Src/Common/Controls/XMLViews/XmlBrowseRDEView.cs index bff84e09d8..1a1a91ab77 100644 --- a/Src/Common/Controls/XMLViews/XmlBrowseRDEView.cs +++ b/Src/Common/Controls/XMLViews/XmlBrowseRDEView.cs @@ -104,8 +104,8 @@ public override void Init(XmlNode nodeSpec, int hvoRoot, int fakeFlid, // Use the ones in fakeFlid, and any we create. base.Init(nodeSpec, hvoRoot, fakeFlid, cache, mediator, bv); - Subscriber.Subscribe(EventConstants.DeleteRecord, DeleteRecord); - Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing); + Subscriber.Subscribe(EventConstants.DeleteRecord, DeleteRecord, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing, m_propertyTable.GetWindow()); } #endregion Construction, initialization, and disposal. diff --git a/Src/Common/Controls/XMLViews/XmlBrowseViewBase.cs b/Src/Common/Controls/XMLViews/XmlBrowseViewBase.cs index 921c316730..ec4dc567ce 100644 --- a/Src/Common/Controls/XMLViews/XmlBrowseViewBase.cs +++ b/Src/Common/Controls/XMLViews/XmlBrowseViewBase.cs @@ -2085,9 +2085,9 @@ public override void Init(Mediator mediator, PropertyTable propertyTable, XmlNod SetSelectedRowHighlighting();//read the property table - Subscriber.Subscribe(EventConstants.SaveScrollPosition, SaveScrollPosition); - Subscriber.Subscribe(EventConstants.RestoreScrollPosition, RestoreScrollPosition); - Subscriber.Subscribe(EventConstants.PrepareToRefresh, PrepareToRefresh); + Subscriber.Subscribe(EventConstants.SaveScrollPosition, SaveScrollPosition, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.RestoreScrollPosition, RestoreScrollPosition, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.PrepareToRefresh, PrepareToRefresh, m_propertyTable.GetWindow()); } #endregion XCore Colleague overrides diff --git a/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs b/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs index fdbb7a5cb8..613317e2a5 100644 --- a/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs +++ b/Src/Common/Controls/XMLViews/XmlBrowseViewBaseVc.cs @@ -1767,7 +1767,7 @@ public override void DoHotLinkAction(string strData, ISilDataAccess sda) FwLinkArgs linkArgs = new FwLinkArgs(url); linkArgs.DisplayErrorMsg = false; var retObj = new ReturnObject(linkArgs); - Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, retObj)); + Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, retObj, m_xbv?.m_bv?.PropTable?.GetWindow())); if (retObj.ReturnValue) return; } diff --git a/Src/Common/FieldWorks/FieldWorks.cs b/Src/Common/FieldWorks/FieldWorks.cs index 9e01e6df2c..5d69adec14 100644 --- a/Src/Common/FieldWorks/FieldWorks.cs +++ b/Src/Common/FieldWorks/FieldWorks.cs @@ -1849,7 +1849,7 @@ private static ProjectId ShowWelcomeDialog(FwAppArgs args, FwApp startingApp, Pr s_projectId = projectToTry; // Window is open on this project, we must not try to initialize it again. if (Form.ActiveForm is IxWindow mainWindow) { - Publisher.Publish(new PublisherParameterObject(EventConstants.SFMImport)); + Publisher.Publish(new PublisherParameterObject(EventConstants.SFMImport, null, mainWindow)); } else { diff --git a/Src/Common/FwUtils/EndOfActionManager.cs b/Src/Common/FwUtils/EndOfActionManager.cs index d243a62458..7ede4857fb 100644 --- a/Src/Common/FwUtils/EndOfActionManager.cs +++ b/Src/Common/FwUtils/EndOfActionManager.cs @@ -19,7 +19,11 @@ namespace SIL.FieldWorks.Common.FwUtils public sealed class EndOfActionManager { private bool m_initialized = false; - private readonly Dictionary m_events = new Dictionary(); + // The events are grouped by message, and then by scope within each message. At most a message + // is queued once per scope (last one wins). Note: Null is a valid scope, it follows the same + // rules; queued once and last one wins, but null scope is unique in that the message is delivered + // to all subscribers regardless of their scope (null or not null). + private readonly Dictionary> m_events = new Dictionary>(); /// /// Returns the EndOfAction event that is currently being published. @@ -51,8 +55,23 @@ internal void AddEvent(PublisherParameterObject publisherParameterObject) } } - // Add the dictionary entry if the key is not present. Overwrite the value if the key is present. - m_events[publisherParameterObject.Message] = publisherParameterObject.Data; + // The whole parameter object is kept so the delivery scope survives until the idle publish. + // Group by both message and scope: overwrite an already-queued publish only if it has the + // same scope; publishes of the same message from different scopes are all delivered. + if (!m_events.TryGetValue(publisherParameterObject.Message, out var queued)) + { + queued = new List(); + m_events.Add(publisherParameterObject.Message, queued); + } + var sameScopeIndex = queued.FindIndex(e => ReferenceEquals(e.Scope, publisherParameterObject.Scope)); + if (sameScopeIndex >= 0) + { + queued[sameScopeIndex] = publisherParameterObject; + } + else + { + queued.Add(publisherParameterObject); + } } /// Should be private, but is public to support testing. @@ -63,11 +82,14 @@ public bool IdleEndOfAction(object _) { foreach (var orderedEvent in EndOfActionOrder.Order) { - if (m_events.TryGetValue(orderedEvent, out object data)) + if (m_events.TryGetValue(orderedEvent, out var queuedEvents)) { m_events.Remove(orderedEvent); CurrentEndOfActionEvent = orderedEvent; - FwUtils.Publisher.Publish(new PublisherParameterObject(orderedEvent, data)); + foreach (var publisherParameterObject in queuedEvents) + { + FwUtils.Publisher.Publish(publisherParameterObject); + } CurrentEndOfActionEvent = null; } diff --git a/Src/Common/FwUtils/FwUtilsTests/PubSubSystemTests.cs b/Src/Common/FwUtils/FwUtilsTests/PubSubSystemTests.cs index 456032a54d..95ce8fc649 100644 --- a/Src/Common/FwUtils/FwUtilsTests/PubSubSystemTests.cs +++ b/Src/Common/FwUtils/FwUtilsTests/PubSubSystemTests.cs @@ -205,6 +205,147 @@ public void Test_Two_Subscribers_For_MessageOneHandling() Assert.That(subscriber2.One, Is.True); // Did not change. } + /// + /// A scoped publish goes to same-scope and unscoped subscribers, but not to + /// subscribers with a different scope. + /// + [Test] + public void Scoped_Publish_Delivers_Only_To_Matching_Scope_And_Unscoped_Subscribers() + { + // Set up. + var scopeA = new TestPubSubScope(); + var subscriberA = new ScopeAwareSubscriber(scopeA) { One = true }; + var subscriberB = new ScopeAwareSubscriber(new TestPubSubScope()) { One = true }; + var unscopedSubscriber = new ScopeAwareSubscriber(null) { One = true }; + subscriberA.DoSubscriptions(); + subscriberB.DoSubscriptions(); + unscopedSubscriber.DoSubscriptions(); + + // Run test. + FwUtils.Publisher.Publish(new PublisherParameterObject("MessageOne", false, scopeA)); + Assert.That(subscriberA.One, Is.False); // Same scope: delivered. + Assert.That(subscriberB.One, Is.True); // Different scope: not delivered. + Assert.That(unscopedSubscriber.One, Is.False); // Unscoped subscriber: delivered. + + subscriberA.DoUnsubscriptions(); + subscriberB.DoUnsubscriptions(); + unscopedSubscriber.DoUnsubscriptions(); + } + + /// + /// An unscoped publish is process-wide: every subscriber receives it, scoped or not. + /// + [Test] + public void Unscoped_Publish_Delivers_To_Scoped_And_Unscoped_Subscribers() + { + // Set up. + var scopedSubscriber = new ScopeAwareSubscriber(new TestPubSubScope()) { One = true }; + var unscopedSubscriber = new ScopeAwareSubscriber(null) { One = true }; + scopedSubscriber.DoSubscriptions(); + unscopedSubscriber.DoSubscriptions(); + + // Run test. + FwUtils.Publisher.Publish(new PublisherParameterObject("MessageOne", false)); + Assert.That(scopedSubscriber.One, Is.False); // Delivered. + Assert.That(unscopedSubscriber.One, Is.False); // Delivered. + + scopedSubscriber.DoUnsubscriptions(); + unscopedSubscriber.DoUnsubscriptions(); + } + + /// + /// The scope must survive the EndOfActionManager round trip through the idle queue. + /// + [Test] + public void Scope_Is_Preserved_Through_PublishAtEndOfAction() + { + // Set up. + var scopeA = new TestPubSubScope(); + var subscriberA = new ScopeAwareSubscriber(scopeA) { Two = int.MinValue }; + var subscriberB = new ScopeAwareSubscriber(new TestPubSubScope()) { Two = int.MinValue }; + subscriberA.DoSubscriptions(); + subscriberB.DoSubscriptions(); + + FwUtils.Publisher.PublishAtEndOfAction(new PublisherParameterObject(EventConstants.SelectionChanged, int.MaxValue, scopeA)); + + // Nothing is delivered until the idle queue drains. + Assert.That(subscriberA.Two, Is.EqualTo(int.MinValue)); + Assert.That(subscriberB.Two, Is.EqualTo(int.MinValue)); + + // SUT - Process the EndOfActionManager IdleQueue. + FwUtils.Publisher.EndOfActionManager.IdleEndOfAction(null); + + Assert.That(subscriberA.Two, Is.EqualTo(int.MaxValue)); // Same scope: delivered. + Assert.That(subscriberB.Two, Is.EqualTo(int.MinValue)); // Different scope: not delivered. + + subscriberA.DoUnsubscriptions(); + subscriberB.DoUnsubscriptions(); + } + + /// + /// EndOfAction coalescing is per (message, scope): the same message queued from two + /// scopes is delivered to both, while a re-publish from the same scope overwrites. + /// + [Test] + public void PublishAtEndOfAction_Coalesces_Per_Scope() + { + // Set up. + var scopeA = new TestPubSubScope(); + var scopeB = new TestPubSubScope(); + var subscriberA = new ScopeAwareSubscriber(scopeA) { Two = int.MinValue }; + var subscriberB = new ScopeAwareSubscriber(scopeB) { Two = int.MinValue }; + subscriberA.DoSubscriptions(); + subscriberB.DoSubscriptions(); + + FwUtils.Publisher.PublishAtEndOfAction(new PublisherParameterObject(EventConstants.SelectionChanged, 1, scopeA)); + FwUtils.Publisher.PublishAtEndOfAction(new PublisherParameterObject(EventConstants.SelectionChanged, 2, scopeB)); + // Same scope again: coalesces with (overwrites) the first scopeA publish. + FwUtils.Publisher.PublishAtEndOfAction(new PublisherParameterObject(EventConstants.SelectionChanged, 3, scopeA)); + + // SUT - Process the EndOfActionManager IdleQueue. + FwUtils.Publisher.EndOfActionManager.IdleEndOfAction(null); + + Assert.That(subscriberA.Two, Is.EqualTo(3)); // Latest scopeA publish won. + Assert.That(subscriberB.Two, Is.EqualTo(2)); // scopeB publish survived independently. + + subscriberA.DoUnsubscriptions(); + subscriberB.DoUnsubscriptions(); + } + + /// + /// Prefix subscriptions honor the same scope rule as specific subscriptions: + /// scoped publishes skip other-scope prefix subscribers and reach same-scope and + /// unscoped ones; non-matching prefixes are never delivered. + /// + [Test] + public void Prefix_Subscriptions_Honor_Scope() + { + // Set up. + var scopeA = new TestPubSubScope(); + var subscriberA = new PrefixScopeAwareSubscriber(scopeA); + var subscriberB = new PrefixScopeAwareSubscriber(new TestPubSubScope()); + var unscopedSubscriber = new PrefixScopeAwareSubscriber(null); + subscriberA.DoSubscriptions(); + subscriberB.DoSubscriptions(); + unscopedSubscriber.DoSubscriptions(); + + // Run test. + FwUtils.Publisher.Publish(new PublisherParameterObject("PrefixedMessageOne", 7, scopeA)); + Assert.That(subscriberA.LastMessage, Is.EqualTo("PrefixedMessageOne")); // Same scope: delivered. + Assert.That(subscriberB.LastMessage, Is.Null); // Different scope: not delivered. + Assert.That(unscopedSubscriber.LastMessage, Is.EqualTo("PrefixedMessageOne")); // Unscoped subscriber: delivered. + + subscriberA.LastMessage = null; + unscopedSubscriber.LastMessage = null; + FwUtils.Publisher.Publish(new PublisherParameterObject("UnrelatedMessage", 7, scopeA)); + Assert.That(subscriberA.LastMessage, Is.Null); // Prefix does not match: not delivered. + Assert.That(unscopedSubscriber.LastMessage, Is.Null); + + subscriberA.DoUnsubscriptions(); + subscriberB.DoUnsubscriptions(); + unscopedSubscriber.DoUnsubscriptions(); + } + [Test] [TestCase(null)] [TestCase("")] @@ -553,6 +694,81 @@ private void MessageTwoHandler(object newValue) } } + private sealed class TestPubSubScope : IPubSubScope + { + } + + private sealed class PrefixScopeAwareSubscriber + { + private readonly IPubSubScope _scope; + + internal PrefixScopeAwareSubscriber(IPubSubScope scope) + { + _scope = scope; + } + + internal string LastMessage { get; set; } + + /// + /// This is the subscribed prefix handler for messages starting with "PrefixedMessage". + /// + private void PrefixedMessageHandler(string message, object newValue) + { + LastMessage = message; + } + + internal void DoSubscriptions() + { + FwUtils.Subscriber.PrefixSubscribe("PrefixedMessage", PrefixedMessageHandler, _scope); + } + + internal void DoUnsubscriptions() + { + FwUtils.Subscriber.PrefixUnsubscribe("PrefixedMessage", PrefixedMessageHandler); + } + } + + private sealed class ScopeAwareSubscriber + { + private readonly IPubSubScope _scope; + + internal ScopeAwareSubscriber(IPubSubScope scope) + { + _scope = scope; + } + + internal bool One { get; set; } + internal int Two { get; set; } + + /// + /// This is the subscribed message handler for "MessageOne" message. + /// + private void MessageOneHandler(object newValue) + { + One = (bool)newValue; + } + + /// + /// This is the subscribed message handler for the SelectionChanged message. + /// + private void SelectionChangedHandler(object newValue) + { + Two = (int)newValue; + } + + internal void DoSubscriptions() + { + FwUtils.Subscriber.Subscribe("MessageOne", MessageOneHandler, _scope); + FwUtils.Subscriber.Subscribe(EventConstants.SelectionChanged, SelectionChangedHandler, _scope); + } + + internal void DoUnsubscriptions() + { + FwUtils.Subscriber.Unsubscribe("MessageOne", MessageOneHandler); + FwUtils.Subscriber.Unsubscribe(EventConstants.SelectionChanged, SelectionChangedHandler); + } + } + private sealed class EndOfAction_MultipleSubscriber { internal string First { get; set; } diff --git a/Src/Common/FwUtils/IPubSubScope.cs b/Src/Common/FwUtils/IPubSubScope.cs new file mode 100644 index 0000000000..9a2911a510 --- /dev/null +++ b/Src/Common/FwUtils/IPubSubScope.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2026 SIL International +// This software is licensed under the LGPL, version 2.1 or later +// (http://www.gnu.org/licenses/lgpl-2.1.html) + +namespace SIL.FieldWorks.Common.FwUtils +{ + /// + /// Marker interface for objects that scope Pub/Sub delivery. + /// + /// The Publisher and Subscriber are process-wide singletons, but most messages are + /// window-scoped: the legacy Mediator they replace delivered only within one main window. + /// To reproduce that, a subscriber passes its main window when subscribing, and a publisher + /// passes the same window in its PublisherParameterObject; the Publisher then delivers a + /// scoped publish only to subscribers with the identical (reference-equal) scope. + /// A Publisher with a null scope will go to all the message subscribers and a Subscriber + /// with a null scope will receive all the messages, regardless of them being published + /// with or without a scope. + /// Null is for publishers with no window context (e.g. app bootstrap code) and for + /// messages that must reach a window the publisher cannot name (e.g. Send/Receive messages + /// aimed at the project's reopened instance). (Process-wide delivery is a Pub/Sub-era + /// capability, not preserved Mediator behavior. The Mediator had no all-windows broadcast; + /// instead its app-wide coordination was accomplished by iterating FwApp.MainWindows directly.) + /// + /// The scope IS the main window: the XCore IxWindow interface extends this one, so main + /// windows pass this and everything else obtains the window via + /// PropertyTable.GetWindow(). + /// Do not implement this interface on anything that is not a main window. + /// + public interface IPubSubScope + { + } +} diff --git a/Src/Common/FwUtils/ISubscriber.cs b/Src/Common/FwUtils/ISubscriber.cs index e50aaa45f6..9a8586979d 100644 --- a/Src/Common/FwUtils/ISubscriber.cs +++ b/Src/Common/FwUtils/ISubscriber.cs @@ -20,7 +20,11 @@ public interface ISubscriber /// The message being subscribed to receive. /// The method on subscriber to call, when /// has been published - void Subscribe(string message, Action messageHandler); + /// Optional delivery scope (normally the subscriber's main window). + /// When non-null, scoped publishes of are delivered only if they + /// carry the same scope. Null subscribes will receive messages from all publishers. + /// See . + void Subscribe(string message, Action messageHandler, IPubSubScope scope = null); /// /// An object subscribes to messages that begin with using @@ -33,7 +37,11 @@ public interface ISubscriber /// The message prefix being subscribed to receive. /// The method on subscriber to call, when a message that /// begins with has been published. - void PrefixSubscribe(string messagePrefix, Action messageHandler); + /// Optional delivery scope (normally the subscriber's main window). + /// When non-null, scoped publishes of matching messages are delivered only if they + /// carry the same scope. Null subscribes will receive messages from all publishers. + /// See . + void PrefixSubscribe(string messagePrefix, Action messageHandler, IPubSubScope scope = null); /// /// Register end of interest (unsubscribe) of an object in receiving @@ -52,14 +60,16 @@ public interface ISubscriber void PrefixUnsubscribe(string messagePrefix, Action messageHandler); /// - /// Get all current subscriptions. + /// Get all current subscriptions: per message, each handler with the scope it subscribed + /// under (null scope = process-wide). /// - IReadOnlyDictionary>> Subscriptions { get; } + IReadOnlyDictionary, IPubSubScope>> Subscriptions { get; } /// - /// Get all the current prefix subscriptions. If a message starts with one of these + /// Get all the current prefix subscriptions: per prefix, each handler with the scope it + /// subscribed under (null scope = process-wide). If a message starts with one of these /// prefixes then the prefix subscribers will get notified. /// - IReadOnlyDictionary>> PrefixSubscriptions { get; } + IReadOnlyDictionary, IPubSubScope>> PrefixSubscriptions { get; } } } \ No newline at end of file diff --git a/Src/Common/FwUtils/Publisher.cs b/Src/Common/FwUtils/Publisher.cs index 672c763059..873d89e152 100644 --- a/Src/Common/FwUtils/Publisher.cs +++ b/Src/Common/FwUtils/Publisher.cs @@ -26,36 +26,48 @@ public Publisher(ISubscriber subscriber) } /// - /// Publish the message using the new value. + /// Publish the message using the given data. /// /// The message to publish. - /// The new value to send to subscribers. This may be null. - private void PublishMessage(string message, object newValue) + /// The data to send to subscribers. This may be null. + /// The delivery scope. When non-null, only subscribers with the same + /// scope (or none) are invoked; when null, every subscriber is. See . + private void PublishMessage(string message, object data, IPubSubScope scope) { Guard.AgainstNullOrEmptyString(message, nameof(message)); // Check if 'message' was subscribed to. if (_subscriber.Subscriptions.TryGetValue(message, out var subscribers)) { - foreach (var subscriberAction in subscribers.ToList()) + foreach (var subscription in subscribers.ToList()) { - // NB: It is possible that the action's object is disposed, - // but we'll not fret about making sure it isn't disposed, - // but we will expect the subscribers to be well-behaved and unsubscribe, - // when they get disposed. - subscriberAction(newValue); + // A scoped publish is delivered only to subscribers in the same scope (e.g. the + // same main window). A null scope on either end means process-wide delivery. + if (scope == null || subscription.Value == null || ReferenceEquals(scope, subscription.Value)) + { + // NB: It is possible that the action's object is disposed, + // but we'll not fret about making sure it isn't disposed, + // but we will expect the subscribers to be well-behaved and unsubscribe, + // when they get disposed. + subscription.Key(data); + } } } // Check if 'message' contains a prefix that was subscribed to. // Note: ToArray() is important to avoid new entries being added while iterating over the collection. - foreach (KeyValuePair>> entry in _subscriber.PrefixSubscriptions.ToArray()) + foreach (KeyValuePair, IPubSubScope>> entry in _subscriber.PrefixSubscriptions.ToArray()) { if (message.StartsWith(entry.Key)) { - foreach (var subscriberAction in entry.Value.ToList()) + foreach (var subscription in entry.Value.ToList()) { - subscriberAction(message, newValue); + // Same delivery rule as specific subscriptions: a null scope on either end + // means process-wide; otherwise the scopes must be the same window. + if (scope == null || subscription.Value == null || ReferenceEquals(scope, subscription.Value)) + { + subscription.Key(message, data); + } } } } @@ -70,7 +82,7 @@ public void Publish(PublisherParameterObject publisherParameterObject) { Guard.AgainstNull(publisherParameterObject, nameof(publisherParameterObject)); - PublishMessage(publisherParameterObject.Message, publisherParameterObject.Data); + PublishMessage(publisherParameterObject.Message, publisherParameterObject.Data, publisherParameterObject.Scope); } /// @@ -89,7 +101,7 @@ public void Publish(IList publisherParameterObjects) foreach (var publisherParameterObject in publisherParameterObjects) { - PublishMessage(publisherParameterObject.Message, publisherParameterObject.Data); + PublishMessage(publisherParameterObject.Message, publisherParameterObject.Data, publisherParameterObject.Scope); } } #endregion diff --git a/Src/Common/FwUtils/PublisherParameterObject.cs b/Src/Common/FwUtils/PublisherParameterObject.cs index 93a8d8a4a7..a678f4e054 100644 --- a/Src/Common/FwUtils/PublisherParameterObject.cs +++ b/Src/Common/FwUtils/PublisherParameterObject.cs @@ -8,7 +8,7 @@ namespace SIL.FieldWorks.Common.FwUtils { public sealed class PublisherParameterObject { - public PublisherParameterObject(string message, object data = null) + public PublisherParameterObject(string message, object data = null, IPubSubScope scope = null) { if (string.IsNullOrWhiteSpace(message)) { @@ -17,10 +17,18 @@ public PublisherParameterObject(string message, object data = null) Message = message; Data = data; + Scope = scope; } public string Message { get; } public object Data { get; } + + /// + /// Delivery scope (normally the publishing main window). When non-null, only + /// subscribers that subscribed with the same scope (or with none) receive this publish. + /// A null scope publishes to all subscribers, regardless of their scope. See . + /// + public IPubSubScope Scope { get; } } /// diff --git a/Src/Common/FwUtils/Subscriber.cs b/Src/Common/FwUtils/Subscriber.cs index 469129557c..c838a354e4 100644 --- a/Src/Common/FwUtils/Subscriber.cs +++ b/Src/Common/FwUtils/Subscriber.cs @@ -13,10 +13,10 @@ namespace SIL.FieldWorks.Common.FwUtils /// internal sealed class Subscriber : ISubscriber { - private readonly Dictionary>> _subscriptions = - new Dictionary>>(); - private readonly Dictionary>> _prefixSubscriptions = - new Dictionary>>(); + private readonly Dictionary, IPubSubScope>> _subscriptions = + new Dictionary, IPubSubScope>>(); + private readonly Dictionary, IPubSubScope>> _prefixSubscriptions = + new Dictionary, IPubSubScope>>(); #region Implementation of ISubscriber @@ -27,16 +27,20 @@ internal sealed class Subscriber : ISubscriber /// The message being subscribed to receive. /// The method on subscriber to call, when /// has been published - public void Subscribe(string message, Action messageHandler) + /// Optional delivery scope (normally the subscriber's main window). + /// When non-null, scoped publishes of are delivered only if they + /// carry the same scope. Null subscribes will receive messages from all publishers. + /// See . + public void Subscribe(string message, Action messageHandler, IPubSubScope scope = null) { if (!_subscriptions.TryGetValue(message, out var subscribers)) { - subscribers = new HashSet>(); + subscribers = new Dictionary, IPubSubScope>(); _subscriptions.Add(message, subscribers); } // NB: If an ill-behaved subscribing object registers the same delegate more than once - // for the same message, then only one registration will 'take'. - subscribers.Add(messageHandler); + // for the same message, then only one registration will 'take' (the latest scope wins). + subscribers[messageHandler] = scope; } /// @@ -50,16 +54,20 @@ public void Subscribe(string message, Action messageHandler) /// The message prefix being subscribed to receive. /// The method on subscriber to call, when a message that /// begins with has been published. - public void PrefixSubscribe(string messagePrefix, Action messageHandler) + /// Optional delivery scope (normally the subscriber's main window). + /// When non-null, scoped publishes of matching messages are delivered only if they + /// carry the same scope. Null subscribes will receive messages from all publishers. + /// See . + public void PrefixSubscribe(string messagePrefix, Action messageHandler, IPubSubScope scope = null) { if (!_prefixSubscriptions.TryGetValue(messagePrefix, out var prefixSubscribers)) { - prefixSubscribers = new HashSet>(); + prefixSubscribers = new Dictionary, IPubSubScope>(); _prefixSubscriptions.Add(messagePrefix, prefixSubscribers); } // NB: If an ill-behaved subscribing object registers the same delegate more than once - // for the same message, then only one registration will 'take'. - prefixSubscribers.Add(messageHandler); + // for the same message, then only one registration will 'take' (the latest scope wins). + prefixSubscribers[messageHandler] = scope; } /// @@ -103,17 +111,19 @@ public void PrefixUnsubscribe(string messagePrefix, Action messa } /// - /// Get all current subscriptions. + /// Get all current subscriptions: per message, each handler with the scope it subscribed + /// under (null scope = process-wide). /// - public IReadOnlyDictionary>> Subscriptions => - new ReadOnlyDictionary>>(_subscriptions); + public IReadOnlyDictionary, IPubSubScope>> Subscriptions => + new ReadOnlyDictionary, IPubSubScope>>(_subscriptions); /// - /// Get all the current prefix subscriptions. If a message starts with one of these + /// Get all the current prefix subscriptions: per prefix, each handler with the scope it + /// subscribed under (null scope = process-wide). If a message starts with one of these /// prefixes then the prefix subscribers will get notified. /// - public IReadOnlyDictionary>> PrefixSubscriptions => - new ReadOnlyDictionary>>(_prefixSubscriptions); + public IReadOnlyDictionary, IPubSubScope>> PrefixSubscriptions => + new ReadOnlyDictionary, IPubSubScope>>(_prefixSubscriptions); #endregion } diff --git a/Src/FdoUi/FdoUiCore.cs b/Src/FdoUi/FdoUiCore.cs index 44487532a7..6af24c591e 100644 --- a/Src/FdoUi/FdoUiCore.cs +++ b/Src/FdoUi/FdoUiCore.cs @@ -400,7 +400,7 @@ public static CmObjectUi CreateNewUiObject(Mediator mediator, PropertyTable prop try { // Don't postpone PropChanged (cf. LT-22095). - Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, false)); + Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, false, propertyTable.GetWindow())); var cache = propertyTable.GetValue("cache"); switch (classId) { @@ -420,7 +420,7 @@ public static CmObjectUi CreateNewUiObject(Mediator mediator, PropertyTable prop } finally { - Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, true)); + Publisher.Publish(new PublisherParameterObject(EventConstants.PostponePropChanged, true, propertyTable.GetWindow())); } } @@ -1026,7 +1026,7 @@ public bool DeleteUnderlyingObject() object command = this; if (m_command != null) command = m_command; - Publisher.Publish(new PublisherParameterObject(EventConstants.DeleteRecord, command)); + Publisher.Publish(new PublisherParameterObject(EventConstants.DeleteRecord, command, m_propertyTable.GetWindow())); } else { diff --git a/Src/LexText/Interlinear/ConcordanceControl.cs b/Src/LexText/Interlinear/ConcordanceControl.cs index d31b7e573e..1f3536c701 100644 --- a/Src/LexText/Interlinear/ConcordanceControl.cs +++ b/Src/LexText/Interlinear/ConcordanceControl.cs @@ -99,7 +99,7 @@ public override void Init(Mediator mediator, PropertyTable propertyTable, XmlNod // Load any saved settings. LoadSettings(); - Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing); + Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing, m_propertyTable.GetWindow()); } /// diff --git a/Src/LexText/Interlinear/InterlinMaster.cs b/Src/LexText/Interlinear/InterlinMaster.cs index 6c44d978c3..eb48e6eff0 100644 --- a/Src/LexText/Interlinear/InterlinMaster.cs +++ b/Src/LexText/Interlinear/InterlinMaster.cs @@ -775,8 +775,8 @@ public override void Init(Mediator mediator, PropertyTable propertyTable, XmlNod m_fullyInitialized = true; RefreshPaneBar(); - Subscriber.Subscribe(EventConstants.PrepareToRefresh, PrepareToRefresh); - Subscriber.Subscribe(EventConstants.RefreshInterlin, RefreshInterlin); + Subscriber.Subscribe(EventConstants.PrepareToRefresh, PrepareToRefresh, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.RefreshInterlin, RefreshInterlin, m_propertyTable.GetWindow()); } /// @@ -1356,7 +1356,7 @@ protected override void UpdateContextHistory() link.PropertyTableEntries.Add(new Property("InterlinearTab", InterlinearTab.ToString())); Clerk.SelectedRecordChanged(true, true); // make sure we update the record count in the Status bar. - Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, link)); + Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, link, m_propertyTable.GetWindow())); } } diff --git a/Src/LexText/Interlinear/InterlinearTextsRecordClerk.cs b/Src/LexText/Interlinear/InterlinearTextsRecordClerk.cs index 6c8f286f40..7e4077e45e 100644 --- a/Src/LexText/Interlinear/InterlinearTextsRecordClerk.cs +++ b/Src/LexText/Interlinear/InterlinearTextsRecordClerk.cs @@ -41,7 +41,7 @@ public List GetScriptureIds() public override void ActivateUI(bool useRecordTreeBar, bool updateStatusBar = true) { // Only needs to handle changes if this RecordClerk is being used in the GUI. - Subscriber.Subscribe(EventConstants.AddTexts, AddTexts); + Subscriber.Subscribe(EventConstants.AddTexts, AddTexts, m_propertyTable.GetWindow()); base.ActivateUI(useRecordTreeBar, updateStatusBar); } @@ -259,7 +259,7 @@ private bool AddNewText(Command command) { // Tell the user we're turning off the filter, and then do it. MessageBox.Show(ITextStrings.ksTurningOffFilter, ITextStrings.ksNote, MessageBoxButtons.OK); - Publisher.Publish(new PublisherParameterObject(EventConstants.RemoveFilters, this)); + Publisher.Publish(new PublisherParameterObject(EventConstants.RemoveFilters, this, m_propertyTable.GetWindow())); m_activeMenuBarFilter = null; } SaveOnChangeRecord(); // commit any changes before we create a new text. @@ -288,7 +288,7 @@ private bool AddNewText(Command command) return false; if (!InDesiredTool("interlinearEdit")) { - Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, new FwLinkArgs("interlinearEdit", CurrentObject.Guid))); + Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, new FwLinkArgs("interlinearEdit", CurrentObject.Guid), m_propertyTable.GetWindow())); } return true; } diff --git a/Src/LexText/Interlinear/SandboxBase.ComboHandlers.cs b/Src/LexText/Interlinear/SandboxBase.ComboHandlers.cs index d075ee9984..c321b4f89d 100644 --- a/Src/LexText/Interlinear/SandboxBase.ComboHandlers.cs +++ b/Src/LexText/Interlinear/SandboxBase.ComboHandlers.cs @@ -2630,7 +2630,7 @@ private void OnSelectCreateNewEntry(object sender, EventArgs args) private void OnSelectEditLexicalEntry(object sender, EventArgs args) { ILexEntry lexEntry = GetLexEntry(); - Publisher.Publish(new PublisherParameterObject(EventConstants.JumpToPopupLexEntry, lexEntry.Hvo)); + Publisher.Publish(new PublisherParameterObject(EventConstants.JumpToPopupLexEntry, lexEntry.Hvo, m_sandbox.m_propertyTable?.GetWindow())); } private ILexEntry GetLexEntry() diff --git a/Src/LexText/Interlinear/StatisticsView.cs b/Src/LexText/Interlinear/StatisticsView.cs index 386977bec8..8e892bf357 100644 --- a/Src/LexText/Interlinear/StatisticsView.cs +++ b/Src/LexText/Interlinear/StatisticsView.cs @@ -88,7 +88,7 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu mediator.AddColleague(this); //add our current state to the history system string toolName = _propertyTable.GetStringProperty("currentContentControl", ""); - Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, Guid.Empty))); + Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, Guid.Empty), _propertyTable.GetWindow())); } private void RebuildStatisticsTable() diff --git a/Src/LexText/LexTextControls/DataNotebook/AnthroFieldMappingDlg.cs b/Src/LexText/LexTextControls/DataNotebook/AnthroFieldMappingDlg.cs index 2ca2e08adf..c37f0d63ca 100644 --- a/Src/LexText/LexTextControls/DataNotebook/AnthroFieldMappingDlg.cs +++ b/Src/LexText/LexTextControls/DataNotebook/AnthroFieldMappingDlg.cs @@ -279,7 +279,7 @@ private void m_btnAddCustom_Click(object sender, EventArgs e) { // Show the ConfigureCustomFields dialog (by publisher to avoid circular dependencies) Publisher.Publish(new PublisherParameterObject(EventConstants.ConfigureCustomFields, - new Tuple(m_mediator, m_propertyTable, AreaConstants.notebook))); + new Tuple(m_mediator, m_propertyTable, AreaConstants.notebook), m_propertyTable.GetWindow())); // Now, clean up our map of possible field targets and reload the field combo list. List delFields = new List(); foreach (int key in m_mapFlidName.Keys) diff --git a/Src/LexText/LexTextControls/EntryDlgListener.cs b/Src/LexText/LexTextControls/EntryDlgListener.cs index 2a91776b0f..6928c5b16e 100644 --- a/Src/LexText/LexTextControls/EntryDlgListener.cs +++ b/Src/LexText/LexTextControls/EntryDlgListener.cs @@ -34,7 +34,12 @@ protected override string PersistentLabel public InsertEntryDlgListener() { - Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector); + } + + public override void Init(Mediator mediator, PropertyTable propertyTable, System.Xml.XmlNode configurationParameters) + { + base.Init(mediator, propertyTable, configurationParameters); + Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector, propertyTable.GetWindow()); } #endregion Construction and Initialization diff --git a/Src/LexText/LexTextControls/InsertEntryDlg.cs b/Src/LexText/LexTextControls/InsertEntryDlg.cs index 0d748e843a..7b18916e13 100644 --- a/Src/LexText/LexTextControls/InsertEntryDlg.cs +++ b/Src/LexText/LexTextControls/InsertEntryDlg.cs @@ -1428,7 +1428,7 @@ private void InsertEntryDlg_Closing(object sender, CancelEventArgs e) CreateNewEntry(); if (DialogResult == DialogResult.Retry) { - Publisher.Publish(new PublisherParameterObject(EventConstants.JumpToPopupLexEntry, m_entry.Hvo)); + Publisher.Publish(new PublisherParameterObject(EventConstants.JumpToPopupLexEntry, m_entry.Hvo, m_propertyTable.GetWindow())); DialogResult = DialogResult.OK; } break; diff --git a/Src/LexText/LexTextControls/LexImportWizardMarker.cs b/Src/LexText/LexTextControls/LexImportWizardMarker.cs index bed7551572..8ab5facfd7 100644 --- a/Src/LexText/LexTextControls/LexImportWizardMarker.cs +++ b/Src/LexText/LexTextControls/LexImportWizardMarker.cs @@ -1061,7 +1061,7 @@ private void btnAddCustomField_Click(object sender, EventArgs e) return; } Publisher.Publish(new PublisherParameterObject(EventConstants.ConfigureCustomFields, - new Tuple(med, m_propertyTable, AreaConstants.lexicon))); + new Tuple(med, m_propertyTable, AreaConstants.lexicon), m_propertyTable.GetWindow())); // The above call can cause the Mediator to 'go away', so check it and // restore the member variable for everyone else who may be surprised diff --git a/Src/LexText/LexTextControls/RecordDlgListener.cs b/Src/LexText/LexTextControls/RecordDlgListener.cs index 82458b4de4..b352675884 100644 --- a/Src/LexText/LexTextControls/RecordDlgListener.cs +++ b/Src/LexText/LexTextControls/RecordDlgListener.cs @@ -30,7 +30,12 @@ protected override string PersistentLabel public InsertRecordDlgListener() { - Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector); + } + + public override void Init(Mediator mediator, PropertyTable propertyTable, System.Xml.XmlNode configurationParameters) + { + base.Init(mediator, propertyTable, configurationParameters); + Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector, propertyTable.GetWindow()); } #endregion Construction and Initialization diff --git a/Src/LexText/LexTextDll/AreaListener.cs b/Src/LexText/LexTextDll/AreaListener.cs index bd454aa5a4..1fd563f85f 100644 --- a/Src/LexText/LexTextDll/AreaListener.cs +++ b/Src/LexText/LexTextDll/AreaListener.cs @@ -154,11 +154,11 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu mediator.AddColleague(this); m_ctotalLists = 0; m_ccustomLists = 0; - Subscriber.Subscribe(EventConstants.SetInitialContentObject, SetInitialContentObject); - Subscriber.Subscribe(EventConstants.SetToolFromName, SetToolFromName); - Subscriber.Subscribe(EventConstants.ReloadAreaTools, ReloadAreaTools); - Subscriber.Subscribe(EventConstants.GetContentControlParameters, GetContentControlParameters); - Subscriber.Subscribe(EventConstants.GetToolForList, GetToolForList); + Subscriber.Subscribe(EventConstants.SetInitialContentObject, SetInitialContentObject, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.SetToolFromName, SetToolFromName, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.ReloadAreaTools, ReloadAreaTools, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.GetContentControlParameters, GetContentControlParameters, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.GetToolForList, GetToolForList, m_propertyTable.GetWindow()); } private DateTime m_lastToolChange = DateTime.MinValue; diff --git a/Src/LexText/Lexicon/FLExBridgeListener.cs b/Src/LexText/Lexicon/FLExBridgeListener.cs index d36413bec7..b3ca8c942f 100644 --- a/Src/LexText/Lexicon/FLExBridgeListener.cs +++ b/Src/LexText/Lexicon/FLExBridgeListener.cs @@ -348,7 +348,7 @@ public bool OnFLExBridge(object commandObject) if (conflictOccurred) { // Send a message for the reopened instance to display the message viewer (used to be conflict report), - // we have been disposed by now + // we have been disposed by now. Publisher.Publish(new PublisherParameterObject(EventConstants.ViewMessages, null)); } } @@ -498,7 +498,7 @@ public bool OnLiftBridge(object argument) if (conflictOccurred) { // Send a message for the reopened instance to display the message viewer (used to be conflict report), - // we have been disposed by now + // we have been disposed by now. Publisher.Publish(new PublisherParameterObject(EventConstants.ViewLiftMessages, null)); } } @@ -1512,7 +1512,7 @@ private void JumpToFlexObject(object sender, FLExJumpEventArgs e) if (!string.IsNullOrEmpty(e.JumpUrl)) { var args = new LocalLinkArgs { Link = e.JumpUrl }; - Publisher.Publish(new PublisherParameterObject(EventConstants.HandleLocalHotlink, args)); + Publisher.Publish(new PublisherParameterObject(EventConstants.HandleLocalHotlink, args, _propertyTable.GetWindow())); } } diff --git a/Src/LexText/Lexicon/LexReferenceMultiSlice.cs b/Src/LexText/Lexicon/LexReferenceMultiSlice.cs index f113ad3345..c75bb2ccb8 100644 --- a/Src/LexText/Lexicon/LexReferenceMultiSlice.cs +++ b/Src/LexText/Lexicon/LexReferenceMultiSlice.cs @@ -757,7 +757,7 @@ public void HandleMoreMenuItem(object sender, EventArgs ea) ILexRefType newKid = list.Services.GetInstance().Create(); list.PossibilitiesOS.Add(newKid); m_cache.DomainDataByFlid.EndUndoTask(); - Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, new FwLinkArgs("lexRefEdit", newKid.Guid))); + Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, new FwLinkArgs("lexRefEdit", newKid.Guid), m_propertyTable.GetWindow())); } protected void ExpandNewNode() diff --git a/Src/LexText/Lexicon/LexReferenceSequenceView.cs b/Src/LexText/Lexicon/LexReferenceSequenceView.cs index 214f3b458e..0d57fe497f 100644 --- a/Src/LexText/Lexicon/LexReferenceSequenceView.cs +++ b/Src/LexText/Lexicon/LexReferenceSequenceView.cs @@ -85,7 +85,7 @@ protected override void Delete() { if (m_displayParent != null && hvoObj == m_displayParent.Hvo) // We need to handle this the same way as the delete command in the slice menu. - Publisher.Publish(new PublisherParameterObject(EventConstants.DataTreeDelete, null)); + Publisher.Publish(new PublisherParameterObject(EventConstants.DataTreeDelete, null, m_propertyTable.GetWindow())); else DeleteObjectFromVector(sel, cvsli, hvoObj, LexEdStrings.ksUndoDeleteRef, LexEdStrings.ksRedoDeleteRef); } diff --git a/Src/LexText/Lexicon/ReversalListener.cs b/Src/LexText/Lexicon/ReversalListener.cs index 6d046ddc96..9588fea8b2 100644 --- a/Src/LexText/Lexicon/ReversalListener.cs +++ b/Src/LexText/Lexicon/ReversalListener.cs @@ -449,7 +449,7 @@ public override void ChangeOwningObject(Guid newGuid, bool updateAndNotify, Dict SetOwningObject(newOwningObj, updateAndNotify); // Reloads and sorts the list. m_propertyTable.SetProperty("ActiveClerkOwningObject", newOwningObj, true); m_propertyTable.SetPropertyPersistence("ActiveClerkOwningObject", false); - Publisher.Publish(new PublisherParameterObject(EventConstants.ClerkOwningObjChanged, this)); + Publisher.Publish(new PublisherParameterObject(EventConstants.ClerkOwningObjChanged, this, m_propertyTable.GetWindow())); } else { diff --git a/Src/LexText/Morphology/MasterCatDlgListener.cs b/Src/LexText/Morphology/MasterCatDlgListener.cs index 540454fd7d..020593cca5 100644 --- a/Src/LexText/Morphology/MasterCatDlgListener.cs +++ b/Src/LexText/Morphology/MasterCatDlgListener.cs @@ -35,7 +35,12 @@ protected override string PersistentLabel /// public MasterCatDlgListener() { - Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector); + } + + public override void Init(Mediator mediator, PropertyTable propertyTable, System.Xml.XmlNode configurationParameters) + { + base.Init(mediator, propertyTable, configurationParameters); + Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector, propertyTable.GetWindow()); } #endregion Construction and Initialization diff --git a/Src/LexText/Morphology/MasterDlgListener.cs b/Src/LexText/Morphology/MasterDlgListener.cs index 841bb7867e..bec52ab696 100644 --- a/Src/LexText/Morphology/MasterDlgListener.cs +++ b/Src/LexText/Morphology/MasterDlgListener.cs @@ -155,7 +155,7 @@ protected virtual void Dispose(bool disposing) /// /// Initialize the IxCoreColleague object. /// - public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configurationParameters) + public virtual void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configurationParameters) { CheckDisposed(); diff --git a/Src/LexText/Morphology/MasterInflFeatDlgListener.cs b/Src/LexText/Morphology/MasterInflFeatDlgListener.cs index 9f7f22e245..0b0238b035 100644 --- a/Src/LexText/Morphology/MasterInflFeatDlgListener.cs +++ b/Src/LexText/Morphology/MasterInflFeatDlgListener.cs @@ -36,7 +36,12 @@ protected override string PersistentLabel /// public MasterInflFeatDlgListener() { - Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector); + } + + public override void Init(Mediator mediator, PropertyTable propertyTable, System.Xml.XmlNode configurationParameters) + { + base.Init(mediator, propertyTable, configurationParameters); + Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector, propertyTable.GetWindow()); } #endregion Construction and Initialization diff --git a/Src/LexText/Morphology/MasterPhonFeatDlgListener.cs b/Src/LexText/Morphology/MasterPhonFeatDlgListener.cs index 1e36f1471c..35a740af96 100644 --- a/Src/LexText/Morphology/MasterPhonFeatDlgListener.cs +++ b/Src/LexText/Morphology/MasterPhonFeatDlgListener.cs @@ -39,7 +39,12 @@ protected override string PersistentLabel /// public MasterPhonFeatDlgListener() { - Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector); + } + + public override void Init(Mediator mediator, PropertyTable propertyTable, System.Xml.XmlNode configurationParameters) + { + base.Init(mediator, propertyTable, configurationParameters); + Subscriber.Subscribe(EventConstants.DialogInsertItemInVector, DialogInsertItemInVector, propertyTable.GetWindow()); } #endregion Construction and Initialization diff --git a/Src/LexText/Morphology/ParserAnalysisRemover.cs b/Src/LexText/Morphology/ParserAnalysisRemover.cs index 95937434a8..e1a7ac110c 100644 --- a/Src/LexText/Morphology/ParserAnalysisRemover.cs +++ b/Src/LexText/Morphology/ParserAnalysisRemover.cs @@ -103,7 +103,7 @@ public void Process() m_dlg.ProgressBar.Step = 1; // stop parser if it's running. - Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser)); + Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser, null, m_dlg.PropTable?.GetWindow())); NonUndoableUnitOfWorkHelper.Do(cache.ActionHandlerAccessor, () => { @@ -125,7 +125,7 @@ public void Process() }); // Interlin display may be affected. - Publisher.Publish(new PublisherParameterObject(EventConstants.RefreshInterlin, null)); + Publisher.Publish(new PublisherParameterObject(EventConstants.RefreshInterlin, null, m_dlg.PropTable?.GetWindow())); } #endregion IUtility implementation diff --git a/Src/LexText/Morphology/RespellerDlg.cs b/Src/LexText/Morphology/RespellerDlg.cs index 1ed033efd6..461eb1c3bc 100644 --- a/Src/LexText/Morphology/RespellerDlg.cs +++ b/Src/LexText/Morphology/RespellerDlg.cs @@ -572,7 +572,7 @@ private void m_btnApply_Click(object sender, EventArgs e) // On the other hand, we don't want to update the new wordform until after DoIt...it might not exist before, // and we won't be messing up any existing occurrences. - Publisher.Publish(new PublisherParameterObject(EventConstants.ItemDataModified, m_cache.ServiceLocator.GetObject(m_respellUndoaction.NewWordform))); + Publisher.Publish(new PublisherParameterObject(EventConstants.ItemDataModified, m_cache.ServiceLocator.GetObject(m_respellUndoaction.NewWordform), m_propertyTable.GetWindow())); ChangesWereMade = true; @@ -1537,10 +1537,10 @@ private void CoreDoIt(ProgressDialogWorkingOn progress, Mediator mediator) } else { - Publisher.Publish(new PublisherParameterObject(EventConstants.ItemDataModified, wfOld)); + Publisher.Publish(new PublisherParameterObject(EventConstants.ItemDataModified, wfOld, RespellSda?.PropTable?.GetWindow())); } } - Publisher.Publish(new PublisherParameterObject(EventConstants.ItemDataModified, wfNew)); + Publisher.Publish(new PublisherParameterObject(EventConstants.ItemDataModified, wfNew, RespellSda?.PropTable?.GetWindow())); uuow.RollBack = false; } } diff --git a/Src/LexText/Morphology/UserAnalysisRemover.cs b/Src/LexText/Morphology/UserAnalysisRemover.cs index 12a255178f..5e9e611690 100644 --- a/Src/LexText/Morphology/UserAnalysisRemover.cs +++ b/Src/LexText/Morphology/UserAnalysisRemover.cs @@ -125,7 +125,7 @@ public void Process() } }); - Publisher.Publish(new PublisherParameterObject(EventConstants.RefreshInterlin, null)); + Publisher.Publish(new PublisherParameterObject(EventConstants.RefreshInterlin, null, m_dlg.PropTable?.GetWindow())); } diff --git a/Src/LexText/ParserUI/ImportWordSetDlg.cs b/Src/LexText/ParserUI/ImportWordSetDlg.cs index 76f27a1710..a59b9878fc 100644 --- a/Src/LexText/ParserUI/ImportWordSetDlg.cs +++ b/Src/LexText/ParserUI/ImportWordSetDlg.cs @@ -32,6 +32,7 @@ public class ImportWordSetDlg : Form /// xCore Mediator. /// protected Mediator m_mediator; + protected PropertyTable m_propertyTable; /// /// /// @@ -72,6 +73,7 @@ public ImportWordSetDlg(Mediator mediator, PropertyTable propertyTable) { //InitializeComponent(); m_mediator = mediator; + m_propertyTable = propertyTable; m_cache = propertyTable.GetValue("cache"); m_helpTopicProvider = propertyTable.GetValue("HelpTopicProvider"); @@ -208,10 +210,10 @@ private void btnImport_Click(object sender, System.EventArgs e) return; } - Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser)); + Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser, null, m_propertyTable.GetWindow())); CreateWordsetFromFiles(m_paths); - Publisher.Publish(new PublisherParameterObject(EventConstants.FilterListChanged, null)); // let record clerk know the list of filters has changed. + Publisher.Publish(new PublisherParameterObject(EventConstants.FilterListChanged, null, m_propertyTable.GetWindow())); // let record clerk know the list of filters has changed. DialogResult = DialogResult.OK; } diff --git a/Src/LexText/ParserUI/ParserListener.cs b/Src/LexText/ParserUI/ParserListener.cs index 9b4adfc524..7658879ab0 100644 --- a/Src/LexText/ParserUI/ParserListener.cs +++ b/Src/LexText/ParserUI/ParserListener.cs @@ -86,9 +86,9 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu m_sda = m_cache.MainCacheAccessor; m_sda.AddNotification(this); - Subscriber.Subscribe(EventConstants.StopParser, StopParser); - Subscriber.Subscribe(EventConstants.RefreshPopupWindowFonts, RefreshPopupWindowFonts); - Subscriber.Subscribe(EventConstants.Idle, Idle); + Subscriber.Subscribe(EventConstants.StopParser, StopParser, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.RefreshPopupWindowFonts, RefreshPopupWindowFonts, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.Idle, Idle, m_propertyTable.GetWindow()); } /// diff --git a/Src/XCore/xCoreInterfaces/IxCoreColleague.cs b/Src/XCore/xCoreInterfaces/IxCoreColleague.cs index 727399793c..40ed6d03ae 100644 --- a/Src/XCore/xCoreInterfaces/IxCoreColleague.cs +++ b/Src/XCore/xCoreInterfaces/IxCoreColleague.cs @@ -11,6 +11,7 @@ using System.Xml; using System.Windows.Forms; using System.Collections.Generic; +using SIL.FieldWorks.Common.FwUtils; namespace XCore { @@ -137,8 +138,10 @@ PropertyTable PropTable /// /// This is an interface implemented by xWindow (and perhaps other main window classes?) /// that allows a few of their key functions to be called by things that don't reference xCore. + /// A main window is also the Pub/Sub delivery scope (IPubSubScope): messages published with a + /// window as their scope are delivered only to subscribers that subscribed with the same window. /// - public interface IxWindow : IMediatorProvider, IPropertyTableProvider + public interface IxWindow : IMediatorProvider, IPropertyTableProvider, IPubSubScope { /// /// Call this for the duration of a block of code where we don't want idle events. diff --git a/Src/XCore/xCoreInterfaces/PropertyTable.cs b/Src/XCore/xCoreInterfaces/PropertyTable.cs index 875675fefe..eb312f676b 100644 --- a/Src/XCore/xCoreInterfaces/PropertyTable.cs +++ b/Src/XCore/xCoreInterfaces/PropertyTable.cs @@ -323,6 +323,22 @@ public T GetValue(string name) return GetValue(name, SettingsGroup.BestSettings); } + /// + /// The main window owning this property table (each main window registers itself under + /// "window" in its own table), or null when unavailable — a private or test property + /// table, or early window construction. Being an IxWindow, the result also serves as the + /// Pub/Sub delivery scope; a null scope means process-wide delivery, so "unavailable" + /// can only widen delivery, never lose a message. + /// + /// Null-safe by design: unlike GetValue<T>, this never throws when the + /// "window" property holds a non-window (as some test fixtures do). + public IxWindow GetWindow() + { + CheckDisposed(); + + return GetValue("window") as IxWindow; + } + /// /// Get the value of the property of the specified settingsGroup. /// diff --git a/Src/XCore/xWindow.cs b/Src/XCore/xWindow.cs index eb6b73f9e5..8c4c4792a2 100644 --- a/Src/XCore/xWindow.cs +++ b/Src/XCore/xWindow.cs @@ -441,8 +441,8 @@ private void BootstrapPart1() InitializeComponent(); - Subscriber.Subscribe(EventConstants.PrepareToRefresh, PrepareToRefresh); - Subscriber.Subscribe(EventConstants.SetInitialContentObject, SetInitialContentObject); + Subscriber.Subscribe(EventConstants.PrepareToRefresh, PrepareToRefresh, this); + Subscriber.Subscribe(EventConstants.SetInitialContentObject, SetInitialContentObject, this); } /// @@ -802,7 +802,7 @@ protected virtual void LoadUIFromXmlDocument(XmlDocument configuration, string c // Add the content control // Note: We should be able to do it directly, since everything needed is in the default properties. - Publisher.Publish(new PublisherParameterObject(EventConstants.SetInitialContentObject, m_windowConfigurationNode)); + Publisher.Publish(new PublisherParameterObject(EventConstants.SetInitialContentObject, m_windowConfigurationNode, this)); m_sidebarAdapter.FinishInit(); m_menuBarAdapter.FinishInit(); @@ -1230,8 +1230,10 @@ protected ChoiceGroupCollection MakeGroupSet(XmlNode m_windowConfigurationNode, /// private void SetInitialContentObject(object windowConfigurationNode) { - // We only want to handle this if there are no other listeners - if (Subscriber.Subscriptions[EventConstants.SetInitialContentObject].Count > 1) + // We only want to handle this if there are no other listeners for this window + // (count only subscriptions in this window's scope or with no scope). + if (Subscriber.Subscriptions[EventConstants.SetInitialContentObject] + .Count(subscription => subscription.Value == null || ReferenceEquals(subscription.Value, this)) > 1) return; CheckDisposed(); @@ -1789,8 +1791,8 @@ public void SynchronizedOnIdleTime() UpdateControls(); - // Notify any subscribers that the application is idle. - Publisher.Publish(new PublisherParameterObject(EventConstants.Idle, null)); + // Notify this window's subscribers that the application is idle. + Publisher.Publish(new PublisherParameterObject(EventConstants.Idle, null, this)); } /// @@ -1912,7 +1914,7 @@ public bool OnCloseWindow(object sender) { CheckDisposed(); - Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser)); + Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser, null, this)); this.Close(); return true; @@ -2243,7 +2245,7 @@ protected virtual void XWindow_Closing(object sender, CancelEventArgs e) m_widgetUpdateTimer.Enabled = false; e.Cancel = false; - Publisher.Publish(new PublisherParameterObject(EventConstants.ConsideringClosing, e)); + Publisher.Publish(new PublisherParameterObject(EventConstants.ConsideringClosing, e, this)); if (e.Cancel) { m_widgetUpdateTimer.Enabled = true; diff --git a/Src/xWorks/CustomListDlg.cs b/Src/xWorks/CustomListDlg.cs index 803c2861cc..3ba3bf6896 100644 --- a/Src/xWorks/CustomListDlg.cs +++ b/Src/xWorks/CustomListDlg.cs @@ -315,7 +315,7 @@ protected virtual void DoOKAction() private void ReloadListsArea() { - Publisher.Publish(new PublisherParameterObject(EventConstants.ReloadAreaTools, "lists")); + Publisher.Publish(new PublisherParameterObject(EventConstants.ReloadAreaTools, "lists", m_propertyTable.GetWindow())); } /// diff --git a/Src/xWorks/DTMenuHandler.cs b/Src/xWorks/DTMenuHandler.cs index 5bd344373b..ea3618f3e8 100644 --- a/Src/xWorks/DTMenuHandler.cs +++ b/Src/xWorks/DTMenuHandler.cs @@ -173,7 +173,7 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu m_mediator = mediator; m_propertyTable = propertyTable; m_configuration = configurationParameters; - Subscriber.Subscribe(EventConstants.DataTreeDelete, DataTreeDelete); + Subscriber.Subscribe(EventConstants.DataTreeDelete, DataTreeDelete, m_propertyTable.GetWindow()); } /// diff --git a/Src/xWorks/DictionaryConfigurationController.cs b/Src/xWorks/DictionaryConfigurationController.cs index 5c05770e49..0ef02cf553 100644 --- a/Src/xWorks/DictionaryConfigurationController.cs +++ b/Src/xWorks/DictionaryConfigurationController.cs @@ -374,7 +374,7 @@ public DictionaryConfigurationController(IDictionaryConfigurationView view, Prop SaveModel(); MasterRefreshRequired = false; // We're reloading the whole app, that's refresh enough View.Close(); - Publisher.Publish(new PublisherParameterObject(EventConstants.ReloadAreaTools, "lists")); + Publisher.Publish(new PublisherParameterObject(EventConstants.ReloadAreaTools, "lists", _propertyTable.GetWindow())); }; SetManagerTypeInfo(dialog); dialog.ShowDialog(View as Form); diff --git a/Src/xWorks/DictionaryConfigurationMigrators/PreHistoricMigrator.cs b/Src/xWorks/DictionaryConfigurationMigrators/PreHistoricMigrator.cs index 33e86850c1..d57a72467a 100644 --- a/Src/xWorks/DictionaryConfigurationMigrators/PreHistoricMigrator.cs +++ b/Src/xWorks/DictionaryConfigurationMigrators/PreHistoricMigrator.cs @@ -128,7 +128,7 @@ private XmlNode GetConfigureLayoutsNodeForTool(string tool) { var collector = new XmlNode[1]; var parameter = new Tuple("lexicon", tool, collector); - Publisher.Publish(new PublisherParameterObject(EventConstants.GetContentControlParameters, parameter)); + Publisher.Publish(new PublisherParameterObject(EventConstants.GetContentControlParameters, parameter, m_propertyTable.GetWindow())); var controlNode = collector[0]; var parameters = controlNode.SelectSingleNode("parameters"); var configureLayouts = XmlUtils.FindNode(parameters, "configureLayouts"); diff --git a/Src/xWorks/ExportDialog.cs b/Src/xWorks/ExportDialog.cs index 122b5220d4..5f73d91a64 100644 --- a/Src/xWorks/ExportDialog.cs +++ b/Src/xWorks/ExportDialog.cs @@ -518,7 +518,7 @@ private Control EnsureViewInfo() } var collector = new XmlNode[1]; var parameter = new Tuple(area, tool, collector); - Publisher.Publish(new PublisherParameterObject(EventConstants.GetContentControlParameters, parameter)); + Publisher.Publish(new PublisherParameterObject(EventConstants.GetContentControlParameters, parameter, m_propertyTable.GetWindow())); var controlNode = collector[0]; Debug.Assert(controlNode != null); XmlNode dynLoaderNode = controlNode.SelectSingleNode("dynamicloaderinfo"); @@ -1078,7 +1078,7 @@ private object ExportGrammarSketch(IThreadedProgress progress, object[] args) var sXslts = (string)args[2]; m_progressDlg = progress; var parameter = new Tuple(sDataType, outPath, sXslts); - Publisher.Publish(new PublisherParameterObject(EventConstants.SaveAsWebpage, parameter)); + Publisher.Publish(new PublisherParameterObject(EventConstants.SaveAsWebpage, parameter, m_propertyTable.GetWindow())); m_progressDlg.Step(1000); return null; } diff --git a/Src/xWorks/FwXApp.cs b/Src/xWorks/FwXApp.cs index 3031c04195..6278bbc0e7 100644 --- a/Src/xWorks/FwXApp.cs +++ b/Src/xWorks/FwXApp.cs @@ -200,7 +200,7 @@ public override void HandleIncomingLink(FwLinkArgs link) FwXWindow fwxwnd = m_rgMainWindows.Count > 0 ? (FwXWindow)m_rgMainWindows[0] : null; if (fwxwnd != null) { - Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, link)); + Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, link, fwxwnd)); bool topmost = fwxwnd.TopMost; fwxwnd.TopMost = true; fwxwnd.TopMost = topmost; diff --git a/Src/xWorks/FwXWindow.cs b/Src/xWorks/FwXWindow.cs index 67f595cddd..abac6d170a 100644 --- a/Src/xWorks/FwXWindow.cs +++ b/Src/xWorks/FwXWindow.cs @@ -314,7 +314,7 @@ private void BasicInit(FwApp app) m_propertyTable.SetProperty("App", app, true); m_propertyTable.SetPropertyPersistence("App", false); } - Subscriber.Subscribe(EventConstants.JumpToPopupLexEntry, JumpToPopupLexEntry); + Subscriber.Subscribe(EventConstants.JumpToPopupLexEntry, JumpToPopupLexEntry, this); } /// ------------------------------------------------------------------------------------ @@ -1673,7 +1673,7 @@ public void PrepareToRefresh() // REVIEW (Hasso) 2023.07: we had used MediatorSendMessageToAllNow as this needs to go to everyone right now. This // method on the mediator will not stop when the message is handled, nor is it deferred. // REVIEW: do we need to replicate this urgency in PubSub? - Publisher.Publish(new PublisherParameterObject(EventConstants.PrepareToRefresh)); + Publisher.Publish(new PublisherParameterObject(EventConstants.PrepareToRefresh, null, this)); } /// @@ -1870,7 +1870,7 @@ public bool ShowStylesDialog(string paraStyleName, string charStyleName, (m_app as FwXApp).OnMasterRefresh(null); // Refresh the fonts on popup windows. - Publisher.Publish(new PublisherParameterObject(EventConstants.RefreshPopupWindowFonts, null)); + Publisher.Publish(new PublisherParameterObject(EventConstants.RefreshPopupWindowFonts, null, this)); } return false; // refresh already called if needed } @@ -2136,7 +2136,7 @@ protected override void XWindow_Closing(object sender, System.ComponentModel.Can // into a situation where the main window was disposed of while the parser // thread was trying to execute com calls on the UI thread and using the // main form as the Invoke point. - Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser)); + Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser, null, this)); base.XWindow_Closing(sender, e); } @@ -2260,7 +2260,7 @@ public bool OnFinishedInit() if (m_startupLink != null) { - Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, m_startupLink)); + Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, m_startupLink, this)); } UpdateControls(); return true; diff --git a/Src/xWorks/GeneratedHtmlViewer.cs b/Src/xWorks/GeneratedHtmlViewer.cs index c3c2f0e301..1ece8d24ca 100644 --- a/Src/xWorks/GeneratedHtmlViewer.cs +++ b/Src/xWorks/GeneratedHtmlViewer.cs @@ -789,9 +789,9 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu //add our current state to the history system string toolName = m_propertyTable.GetStringProperty("currentContentControl", ""); - Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, Guid.Empty))); + Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, Guid.Empty), m_propertyTable.GetWindow())); - Subscriber.Subscribe(EventConstants.SaveAsWebpage, SaveAsWebpage); + Subscriber.Subscribe(EventConstants.SaveAsWebpage, SaveAsWebpage, m_propertyTable.GetWindow()); } #if notnow void Browser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) diff --git a/Src/xWorks/LinkListener.cs b/Src/xWorks/LinkListener.cs index c69a9065df..4e4a416c06 100644 --- a/Src/xWorks/LinkListener.cs +++ b/Src/xWorks/LinkListener.cs @@ -195,9 +195,9 @@ public void Init(Mediator mediator, PropertyTable propertyTable, XmlNode configu m_propertyTable.SetProperty("LinkListener", this, true); m_propertyTable.SetPropertyPersistence("LinkListener", false); - Subscriber.Subscribe(EventConstants.AddContextToHistory, AddContextToHistory); - Subscriber.Subscribe(EventConstants.HandleLocalHotlink, HandleLocalHotlink); - Subscriber.Subscribe(EventConstants.FollowLink, FollowLink); + Subscriber.Subscribe(EventConstants.AddContextToHistory, AddContextToHistory, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.HandleLocalHotlink, HandleLocalHotlink, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.FollowLink, FollowLink, m_propertyTable.GetWindow()); } /// @@ -418,7 +418,7 @@ public bool OnTestFollowLink(object unused) CheckDisposed(); LcmCache cache = m_propertyTable.GetValue("cache"); Guid[] guids = (from entry in cache.LanguageProject.LexDbOA.Entries select entry.Guid).ToArray(); - Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, new FwLinkArgs("lexiconEdit", guids[guids.Length - 1]))); + Publisher.Publish(new PublisherParameterObject(EventConstants.FollowLink, new FwLinkArgs("lexiconEdit", guids[guids.Length - 1]), m_propertyTable.GetWindow())); return true; } @@ -512,7 +512,7 @@ private bool FollowActiveLink(bool suspendLoadingRecord) // Thus we've created this method (on AreaListener) which we call through the FwUtils Publisher/Subscriber. var parameters = new object[2]; parameters[0] = majorObject; - Publisher.Publish(new PublisherParameterObject(EventConstants.GetToolForList, parameters)); + Publisher.Publish(new PublisherParameterObject(EventConstants.GetToolForList, parameters, m_propertyTable.GetWindow())); realTool = (string)parameters[1]; break; case RnResearchNbkTags.kClassId: @@ -555,7 +555,7 @@ private bool FollowActiveLink(bool suspendLoadingRecord) true); m_propertyTable.SetPropertyPersistence("SuspendLoadingRecordUntilOnJumpToRecord", false); } - Publisher.Publish(new PublisherParameterObject(EventConstants.SetToolFromName, m_lnkActive.ToolName)); + Publisher.Publish(new PublisherParameterObject(EventConstants.SetToolFromName, m_lnkActive.ToolName, m_propertyTable.GetWindow())); // Note: It can be Guid.Empty in cases where it was never set, // or more likely, when the HVO was set to -1. if (m_lnkActive.TargetGuid != Guid.Empty) diff --git a/Src/xWorks/RecordBrowseView.cs b/Src/xWorks/RecordBrowseView.cs index 6f958c283b..c9cf56c52e 100644 --- a/Src/xWorks/RecordBrowseView.cs +++ b/Src/xWorks/RecordBrowseView.cs @@ -678,8 +678,8 @@ public override void Init(Mediator mediator, PropertyTable propertyTable, XmlNod // so call it again, since we are ready now. ShowRecord(); - Subscriber.Subscribe(EventConstants.ClerkOwningObjChanged, ClerkOwningObjChanged); - Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing); + Subscriber.Subscribe(EventConstants.ClerkOwningObjChanged, ClerkOwningObjChanged, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing, m_propertyTable.GetWindow()); } private void CheckExpectedListItemsClassInSync() diff --git a/Src/xWorks/RecordClerk.cs b/Src/xWorks/RecordClerk.cs index 5d027e8b75..e3744ae20b 100644 --- a/Src/xWorks/RecordClerk.cs +++ b/Src/xWorks/RecordClerk.cs @@ -1375,7 +1375,7 @@ private void UpdateOwningObject(bool fUpdateOwningObjectOnlyIfChanged) } } if (old != newObj) - Publisher.Publish(new PublisherParameterObject(EventConstants.ClerkOwningObjChanged, this)); + Publisher.Publish(new PublisherParameterObject(EventConstants.ClerkOwningObjChanged, this, m_propertyTable.GetWindow())); } } @@ -2102,8 +2102,8 @@ public virtual void ReloadIfNeeded() virtual public void ActivateUI(bool useRecordTreeBar, bool updateStatusBar = true) { // RecordClerk only needs to handle changes if RecordClerk is being used in GUI - Subscriber.Subscribe(EventConstants.FilterListChanged, FilterListChanged); - Subscriber.Subscribe(EventConstants.DeleteRecord, DeleteRecord); + Subscriber.Subscribe(EventConstants.FilterListChanged, FilterListChanged, m_propertyTable.GetWindow()); + Subscriber.Subscribe(EventConstants.DeleteRecord, DeleteRecord, m_propertyTable.GetWindow()); m_fIsActiveInGui = true; CheckDisposed(); @@ -2312,7 +2312,7 @@ private void BroadcastChange(bool suppressFocusChange) { m_list.CurrentIndex = FindClosestValidIndex(idx, cobj); } - Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser)); + Publisher.Publish(new PublisherParameterObject(EventConstants.StopParser, null, m_propertyTable.GetWindow())); } finally { @@ -2539,7 +2539,7 @@ public bool OnInsertItemInVector(object argument) try { var retObj = new ReturnObject(argument); - Publisher.Publish(new PublisherParameterObject(EventConstants.DialogInsertItemInVector, retObj)); + Publisher.Publish(new PublisherParameterObject(EventConstants.DialogInsertItemInVector, retObj, m_propertyTable.GetWindow())); if (retObj.ReturnValue) return true; } @@ -3160,13 +3160,13 @@ private void m_list_AboutToReload(object sender, EventArgs e) // For now, we'll not try to be concerned about restoring scroll position // in a context where we're reloading after suppressing a reload. if (!m_fReloadingDueToMissingObject) - Publisher.Publish(new PublisherParameterObject(EventConstants.SaveScrollPosition, this)); + Publisher.Publish(new PublisherParameterObject(EventConstants.SaveScrollPosition, this, m_propertyTable.GetWindow())); } private void m_list_DoneReload(object sender, EventArgs e) { if (!m_fReloadingDueToMissingObject) - Publisher.Publish(new PublisherParameterObject(EventConstants.RestoreScrollPosition, this)); + Publisher.Publish(new PublisherParameterObject(EventConstants.RestoreScrollPosition, this, m_propertyTable.GetWindow())); } internal ListUpdateHelper UpdateHelper { get; set; } diff --git a/Src/xWorks/RecordDocView.cs b/Src/xWorks/RecordDocView.cs index 0c026e09a0..70b82acf99 100644 --- a/Src/xWorks/RecordDocView.cs +++ b/Src/xWorks/RecordDocView.cs @@ -68,7 +68,7 @@ public override void Init(Mediator mediator, PropertyTable propertyTable, XmlNod InitBase(mediator, propertyTable, configurationParameters); m_fullyInitialized = true; - Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing); + Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing, m_propertyTable.GetWindow()); } protected override void GetMessageAdditionalTargets(System.Collections.Generic.List collector) diff --git a/Src/xWorks/RecordEditView.cs b/Src/xWorks/RecordEditView.cs index 413ac65c64..c728d60213 100644 --- a/Src/xWorks/RecordEditView.cs +++ b/Src/xWorks/RecordEditView.cs @@ -126,7 +126,7 @@ public override void Init(Mediator mediator, PropertyTable propertyTable, XmlNod m_dataEntryForm.StyleSheet = FontHeightAdjuster.StyleSheetFromPropertyTable(m_propertyTable); m_fullyInitialized = true; - Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing); + Subscriber.Subscribe(EventConstants.ConsideringClosing, ConsideringClosing, m_propertyTable.GetWindow()); } /// ----------------------------------------------------------------------------------- diff --git a/Src/xWorks/RecordView.cs b/Src/xWorks/RecordView.cs index e9d55c9800..fff7afb81e 100644 --- a/Src/xWorks/RecordView.cs +++ b/Src/xWorks/RecordView.cs @@ -163,7 +163,7 @@ protected virtual void UpdateContextHistory() if (Clerk.CurrentObject != null) guid = Clerk.CurrentObject.Guid; Clerk.SelectedRecordChanged(true, true); // make sure we update the record count in the Status bar. - Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, guid))); + Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, guid), m_propertyTable.GetWindow())); } } diff --git a/Src/xWorks/XWorksViewBase.cs b/Src/xWorks/XWorksViewBase.cs index e56cc91eee..b0ed0e75f6 100644 --- a/Src/xWorks/XWorksViewBase.cs +++ b/Src/xWorks/XWorksViewBase.cs @@ -565,7 +565,7 @@ private void mp_ShowFirstPaneChanged(object sender, EventArgs e) private void ReloadListsArea() { - Publisher.Publish(new PublisherParameterObject(EventConstants.ReloadAreaTools, "lists")); + Publisher.Publish(new PublisherParameterObject(EventConstants.ReloadAreaTools, "lists", m_propertyTable.GetWindow())); } private void DoDeleteCustomListCmd(ICmPossibilityList curList) diff --git a/Src/xWorks/XhtmlDocView.cs b/Src/xWorks/XhtmlDocView.cs index 63672c5f8d..cb58b39154 100644 --- a/Src/xWorks/XhtmlDocView.cs +++ b/Src/xWorks/XhtmlDocView.cs @@ -713,7 +713,7 @@ private static void JumpToFieldAt(object sender, EventArgs e) } // Jump to field on idle to allow JumpToRecord to finish. object[] arguments = new object[] { fieldObj.Hvo, fieldName, fieldElement.TextContent }; - Publisher.PublishAtEndOfAction(new PublisherParameterObject(EventConstants.JumpToField, arguments)); + Publisher.PublishAtEndOfAction(new PublisherParameterObject(EventConstants.JumpToField, arguments, propertyTable.GetWindow())); } } @@ -1520,7 +1520,7 @@ private string SaveConfiguredXhtmlWithProgress(string configurationFile, bool al { if (progressDlg.IsCanceling) { - Publisher.Publish(new PublisherParameterObject(EventConstants.SetToolFromName, "lexiconEdit")); + Publisher.Publish(new PublisherParameterObject(EventConstants.SetToolFromName, "lexiconEdit", m_propertyTable.GetWindow())); } else { diff --git a/Src/xWorks/XmlDocView.cs b/Src/xWorks/XmlDocView.cs index fdbca0c90a..a543bdfff2 100644 --- a/Src/xWorks/XmlDocView.cs +++ b/Src/xWorks/XmlDocView.cs @@ -798,7 +798,7 @@ protected override void ShowRecord() Guid guid = Guid.Empty; if (clerk.CurrentObject != null) guid = clerk.CurrentObject.Guid; - Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, guid))); + Publisher.Publish(new PublisherParameterObject(EventConstants.AddContextToHistory, new FwLinkArgs(toolName, guid), m_propertyTable.GetWindow())); SelectAndScrollToCurrentRecord(); base.ShowRecord(); @@ -1269,7 +1269,7 @@ public override void Init(Mediator mediator, PropertyTable propertyTable, XmlNod InitBase(mediator, propertyTable, configurationParameters); - Subscriber.Subscribe(EventConstants.ClerkOwningObjChanged, ClerkOwningObjChanged); + Subscriber.Subscribe(EventConstants.ClerkOwningObjChanged, ClerkOwningObjChanged, m_propertyTable.GetWindow()); } ///