From b79256cfa1925d0ae1a5d623b2f8cb41cbedecc8 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Tue, 15 Feb 2022 18:10:57 +0000 Subject: [PATCH] Use event aggregator --- FineCodeCoverage/FineCodeCoverage.csproj | 3 + .../FineCodeCoverage2022.csproj | 3 + .../Events/EventAggregator.cs | 565 +++++++++++++++++ FineCodeCoverageTests/FCCEngine_Tests.cs | 13 - .../FineCodeCoverageTests.csproj | 1 + FineCodeCoverageTests/ScriptManager_Tests.cs | 11 +- FineCodeCoverageTests/packages.config | 1 + SharedProject/Core/CoverageUIEvents.cs | 6 - SharedProject/Core/FCCEngine.cs | 47 +- SharedProject/Core/IFCCEngine.cs | 8 - .../ReportGenerator/ReportGeneratorUtil.cs | 83 +-- .../Core/Utilities/EventAggregator.cs | 567 ++++++++++++++++++ SharedProject/Output/DpiChangedMessage.cs | 10 + SharedProject/Output/EnvironmentFont.cs | 64 ++ .../EnvironmentFontDetailsChangedMessage.cs | 8 + SharedProject/Output/InvokeScriptMessage.cs | 21 + SharedProject/Output/NewReportMessage.cs | 8 + .../Output/ObjectForScriptingMessage.cs | 7 + SharedProject/Output/OutputToolWindow.cs | 25 +- .../Output/OutputToolWindowContext.cs | 5 +- .../Output/OutputToolWindowControl.xaml.cs | 131 ++-- .../Output/OutputToolWindowPackage.cs | 4 +- SharedProject/Output/ReadyForReportMessage.cs | 6 + SharedProject/Output/ScriptManager.cs | 28 +- SharedProject/SharedProject.projitems | 8 + 25 files changed, 1397 insertions(+), 236 deletions(-) create mode 100644 FineCodeCoverageTests/Events/EventAggregator.cs create mode 100644 SharedProject/Core/Utilities/EventAggregator.cs create mode 100644 SharedProject/Output/DpiChangedMessage.cs create mode 100644 SharedProject/Output/EnvironmentFont.cs create mode 100644 SharedProject/Output/EnvironmentFontDetailsChangedMessage.cs create mode 100644 SharedProject/Output/InvokeScriptMessage.cs create mode 100644 SharedProject/Output/NewReportMessage.cs create mode 100644 SharedProject/Output/ObjectForScriptingMessage.cs create mode 100644 SharedProject/Output/ReadyForReportMessage.cs diff --git a/FineCodeCoverage/FineCodeCoverage.csproj b/FineCodeCoverage/FineCodeCoverage.csproj index 9f53747d..78ff6206 100644 --- a/FineCodeCoverage/FineCodeCoverage.csproj +++ b/FineCodeCoverage/FineCodeCoverage.csproj @@ -121,6 +121,9 @@ 3.2.2 + + 1.2.0 + 4.1.1 diff --git a/FineCodeCoverage2022/FineCodeCoverage2022.csproj b/FineCodeCoverage2022/FineCodeCoverage2022.csproj index f9034c69..1ed37bcb 100644 --- a/FineCodeCoverage2022/FineCodeCoverage2022.csproj +++ b/FineCodeCoverage2022/FineCodeCoverage2022.csproj @@ -119,6 +119,9 @@ 3.2.2 + + 1.2.0 + 4.1.1 diff --git a/FineCodeCoverageTests/Events/EventAggregator.cs b/FineCodeCoverageTests/Events/EventAggregator.cs new file mode 100644 index 00000000..84a7a236 --- /dev/null +++ b/FineCodeCoverageTests/Events/EventAggregator.cs @@ -0,0 +1,565 @@ +// ReSharper disable InconsistentNaming +namespace FineCodeCoverageTests.Events +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + /* + * + * License: + * + * Microsoft Public License (MS-PL) + * + * This license governs use of the accompanying software. If you use the software, you + * accept this license. If you do not accept the license, do not use the software. + * + * 1. Definitions + * The terms "reproduce," "reproduction," "derivative works," and "distribution" have the + * same meaning here as under U.S. copyright law. + * A "contribution" is the original software, or any additions or changes to the software. + * A "contributor" is any person that distributes its contribution under this license. + * "Licensed patents" are a contributor's patent claims that read directly on its contribution. + * + * 2. Grant of Rights + * (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + * (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + * + * 3. Conditions and Limitations + * (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + * (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. + * (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. + * (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. + * (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. + * + * Little bit of history: + * EventAggregator origins based on work from StatLight's EventAggregator. Which + * is based on original work by Jermey Miller's EventAggregator in StoryTeller + * with some concepts pulled from Rob Eisenberg in caliburnmicro. + * + * TODO: + * - Possibly provide well defined initial thread marshalling actions (depending on platform (WinForm, WPF, Silverlight, WP7???) + * - Document the public API better. + * + * Thanks to: + * - Jermey Miller - initial implementation + * - Rob Eisenberg - pulled some ideas from the caliburn micro event aggregator + * - Jake Ginnivan - https://github.com/JakeGinnivan - thanks for the pull requests + * + */ + + /// + /// Specifies a class that would like to receive particular messages. + /// + /// The type of message object to subscribe to. +#if WINDOWS_PHONE + public interface IListener +#else + public interface IListener +#endif + { + /// + /// This will be called every time a TMessage is published through the event aggregator + /// + void Handle(TMessage message); + } + + /// + /// Provides a way to add and remove a listener object from the EventAggregator + /// + public interface IEventSubscriptionManager + { + /// + /// Adds the given listener object to the EventAggregator. + /// + /// Object that should be implementing IListener(of T's), this overload is used when your listeners to multiple message types + /// determines if the EventAggregator should hold a weak or strong reference to the listener object. If null it will use the Config level option unless overriden by the parameter. + /// Returns the current IEventSubscriptionManager to allow for easy fluent additions. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + IEventSubscriptionManager AddListener(object listener, bool? holdStrongReference = null); + + /// + /// Adds the given listener object to the EventAggregator. + /// + /// Listener Message type + /// + /// determines if the EventAggregator should hold a weak or strong reference to the listener object. If null it will use the Config level option unless overriden by the parameter. + /// Returns the current IEventSubscriptionManager to allow for easy fluent additions. + IEventSubscriptionManager AddListener(IListener listener, bool? holdStrongReference = null); + + /// + /// Removes the listener object from the EventAggregator + /// + /// The object to be removed + /// Returnes the current IEventSubscriptionManager for fluent removals. + IEventSubscriptionManager RemoveListener(object listener); + } + + public interface IEventPublisher + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + void SendMessage(TMessage message, Action marshal = null); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter"), + System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + void SendMessage(Action marshal = null) + where TMessage : new(); + } + + public interface IEventAggregator : IEventPublisher, IEventSubscriptionManager + { + } + + public class EventAggregator : IEventAggregator + { + private readonly ListenerWrapperCollection _listeners; + private readonly Config _config; + + public EventAggregator() + : this(new Config()) + { + } + + public EventAggregator(Config config) + { + _config = config; + _listeners = new ListenerWrapperCollection(); + } + + /// + /// This will send the message to each IListener that is subscribing to TMessage. + /// + /// The type of message being sent + /// The message instance + /// You can optionally override how the message publication action is marshalled + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + public void SendMessage(TMessage message, Action marshal = null) + { + if (marshal == null) + marshal = _config.DefaultThreadMarshaler; + + Call>(message, marshal); + } + + /// + /// This will create a new default instance of TMessage and send the message to each IListener that is subscribing to TMessage. + /// + /// The type of message being sent + /// You can optionally override how the message publication action is marshalled + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed"), + System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", + "CA1004:GenericMethodsShouldProvideTypeParameter")] + public void SendMessage(Action marshal = null) + where TMessage : new() + { + SendMessage(new TMessage(), marshal); + } + + private void Call(object message, Action marshaller) + where TListener : class + { + int listenerCalledCount = 0; + marshaller(() => + { + foreach (ListenerWrapper o in _listeners.Where(o => o.Handles() || o.HandlesMessage(message))) + { + bool wasThisOneCalled; + o.TryHandle(message, out wasThisOneCalled); + if (wasThisOneCalled) + listenerCalledCount++; + } + }); + + var wasAnyListenerCalled = listenerCalledCount > 0; + + if (!wasAnyListenerCalled) + { + _config.OnMessageNotPublishedBecauseZeroListeners(message); + } + } + + public IEventSubscriptionManager AddListener(object listener) + { + return AddListener(listener, null); + } + + public IEventSubscriptionManager AddListener(object listener, bool? holdStrongReference) + { + if (listener == null) throw new ArgumentNullException("listener"); + + bool holdRef = _config.HoldReferences; + if (holdStrongReference.HasValue) + holdRef = holdStrongReference.Value; + bool supportMessageInheritance = _config.SupportMessageInheritance; + _listeners.AddListener(listener, holdRef, supportMessageInheritance); + + return this; + } + + public IEventSubscriptionManager AddListener(IListener listener, bool? holdStrongReference) + { + AddListener((object) listener, holdStrongReference); + + return this; + } + + public IEventSubscriptionManager RemoveListener(object listener) + { + _listeners.RemoveListener(listener); + return this; + } + + /// + /// Wrapper collection of ListenerWrappers to manage things like + /// threadsafe manipulation to the collection, and convenience + /// methods to configure the collection + /// + private class ListenerWrapperCollection : IEnumerable + { + private readonly List _listeners = new List(); + private readonly object _sync = new object(); + + public void RemoveListener(object listener) + { + ListenerWrapper listenerWrapper; + lock (_sync) + if (TryGetListenerWrapperByListener(listener, out listenerWrapper)) + _listeners.Remove(listenerWrapper); + } + + private void RemoveListenerWrapper(ListenerWrapper listenerWrapper) + { + lock (_sync) + _listeners.Remove(listenerWrapper); + } + + public IEnumerator GetEnumerator() + { + lock (_sync) + return _listeners.ToList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private bool ContainsListener(object listener) + { + ListenerWrapper listenerWrapper; + return TryGetListenerWrapperByListener(listener, out listenerWrapper); + } + + private bool TryGetListenerWrapperByListener(object listener, out ListenerWrapper listenerWrapper) + { + lock (_sync) + listenerWrapper = _listeners.SingleOrDefault(x => x.ListenerInstance == listener); + + return listenerWrapper != null; + } + + public void AddListener(object listener, bool holdStrongReference, bool supportMessageInheritance) + { + lock (_sync) + { + + if (ContainsListener(listener)) + return; + + var listenerWrapper = new ListenerWrapper(listener, RemoveListenerWrapper, holdStrongReference, supportMessageInheritance); + if (listenerWrapper.Count == 0) + throw new ArgumentException("IListener is not implemented", "listener"); + _listeners.Add(listenerWrapper); + } + } + } + + #region IReference + + private interface IReference + { + object Target { get; } + } + + private class WeakReferenceImpl : IReference + { + private readonly WeakReference _reference; + + public WeakReferenceImpl(object listener) + { + _reference = new WeakReference(listener); + } + + public object Target + { + get { return _reference.Target; } + } + } + + private class StrongReferenceImpl : IReference + { + private readonly object _target; + + public StrongReferenceImpl(object target) + { + _target = target; + } + + public object Target + { + get { return _target; } + } + } + + #endregion + + private class ListenerWrapper + { + private const string HandleMethodName = "Handle"; + private readonly Action _onRemoveCallback; + private readonly List _handlers = new List(); + private readonly IReference _reference; + + public ListenerWrapper(object listener, Action onRemoveCallback, bool holdReferences, bool supportMessageInheritance) + { + _onRemoveCallback = onRemoveCallback; + + if (holdReferences) + _reference = new StrongReferenceImpl(listener); + else + _reference = new WeakReferenceImpl(listener); + + var listenerInterfaces = TypeHelper.GetBaseInterfaceType(listener.GetType()) + .Where(w => TypeHelper.DirectlyClosesGeneric(w, typeof (IListener<>))); + + foreach (var listenerInterface in listenerInterfaces) + { + var messageType = TypeHelper.GetFirstGenericType(listenerInterface); + var handleMethod = TypeHelper.GetMethod(listenerInterface, HandleMethodName); + + HandleMethodWrapper handler = new HandleMethodWrapper(handleMethod, listenerInterface, messageType,supportMessageInheritance ); + _handlers.Add(handler); + } + } + + public object ListenerInstance + { + get { return _reference.Target; } + } + + public bool Handles() where TListener : class + { + return _handlers.Aggregate(false, (current, handler) => current | handler.Handles()); + } + + public bool HandlesMessage(object message) + { + return message != null && _handlers.Aggregate(false, (current, handler) => current | handler.HandlesMessage(message)); + } + + public void TryHandle(object message, out bool wasHandled) + where TListener : class + { + var target = _reference.Target; + wasHandled = false; + if (target == null) + { + _onRemoveCallback(this); + return; + } + + foreach (var handler in _handlers) + { + bool thisOneHandled = false; + handler.TryHandle(target, message, out thisOneHandled); + wasHandled |= thisOneHandled; + } + } + + public int Count + { + get { return _handlers.Count; } + } + } + + private class HandleMethodWrapper + { + private readonly Type _listenerInterface; + private readonly Type _messageType; + private readonly MethodInfo _handlerMethod; + private readonly bool _supportMessageInheritance; + private readonly Dictionary supportedMessageTypes = new Dictionary(); + + public HandleMethodWrapper(MethodInfo handlerMethod, Type listenerInterface, Type messageType, bool supportMessageInheritance) + { + _handlerMethod = handlerMethod; + _listenerInterface = listenerInterface; + _messageType = messageType; + _supportMessageInheritance = supportMessageInheritance; + supportedMessageTypes[messageType] = true; + } + + public bool Handles() where TListener : class + { + return _listenerInterface == typeof (TListener); + } + + public bool HandlesMessage(object message) + { + if (message == null) + { + return false; + } + + bool handled; + Type messageType = message.GetType(); + bool previousMessageType = supportedMessageTypes.TryGetValue(messageType, out handled); + if (!previousMessageType && _supportMessageInheritance) + { + handled = TypeHelper.IsAssignableFrom(_messageType, messageType); + supportedMessageTypes[messageType] = handled; + } + return handled; + } + + public void TryHandle(object target, object message, out bool wasHandled) + where TListener : class + { + wasHandled = false; + if (target == null) + { + return; + } + + if (!Handles() && !HandlesMessage(message)) return; + + _handlerMethod.Invoke(target, new[] {message}); + wasHandled = true; + } + } + + internal static class TypeHelper + { + internal static IEnumerable GetBaseInterfaceType(Type type) + { + if (type == null) + return new Type[0]; + +#if NETFX_CORE + var interfaces = type.GetTypeInfo().ImplementedInterfaces.ToList(); +#else + var interfaces = type.GetInterfaces().ToList(); +#endif + + foreach (var @interface in interfaces.ToArray()) + { + interfaces.AddRange(GetBaseInterfaceType(@interface)); + } + +#if NETFX_CORE + if (type.GetTypeInfo().IsInterface) +#else + if (type.IsInterface) +#endif + { + interfaces.Add(type); + } + + return interfaces.Distinct(); + } + + internal static bool DirectlyClosesGeneric(Type type, Type openType) + { + if (type == null) + return false; +#if NETFX_CORE + if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == openType) +#else + if (type.IsGenericType && type.GetGenericTypeDefinition() == openType) +#endif + { + return true; + } + + return false; + } + + internal static Type GetFirstGenericType() where T : class + { + return GetFirstGenericType(typeof (T)); + } + + internal static Type GetFirstGenericType(Type type) + { +#if NETFX_CORE + var messageType = type.GetTypeInfo().GenericTypeArguments.First(); +#else + var messageType = type.GetGenericArguments().First(); +#endif + return messageType; + } + + internal static MethodInfo GetMethod(Type type, string methodName) + { +#if NETFX_CORE + var typeInfo = type.GetTypeInfo(); + var handleMethod = typeInfo.GetDeclaredMethod(methodName); +#else + var handleMethod = type.GetMethod(methodName); + +#endif + return handleMethod; + } + + internal static bool IsAssignableFrom(Type type, Type specifiedType) + { +#if NETFX_CORE + return type.GetTypeInfo().IsAssignableFrom(specifiedType.GetTypeInfo()); +#else + return type.IsAssignableFrom(specifiedType); +#endif + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] + public class Config + { + private Action _onMessageNotPublishedBecauseZeroListeners = msg => + { + /* TODO: possibly Trace message?*/ + }; + + public Action OnMessageNotPublishedBecauseZeroListeners + { + get { return _onMessageNotPublishedBecauseZeroListeners; } + set { _onMessageNotPublishedBecauseZeroListeners = value; } + } + + private Action _defaultThreadMarshaler = action => action(); + + public Action DefaultThreadMarshaler + { + get { return _defaultThreadMarshaler; } + set { _defaultThreadMarshaler = value; } + } + + /// + /// If true instructs the EventAggregator to hold onto a reference to all listener objects. You will then have to explicitly remove them from the EventAggrator. + /// If false then a WeakReference is used and the garbage collector can remove the listener when not in scope any longer. + /// + public bool HoldReferences { get; set; } + + /// + /// If true then EventAggregator will support registering listeners for base messages. + /// If false then EventAggregator will only match the message type to the listener. + /// + public bool SupportMessageInheritance { get; set; } + } + } + + +} + +// ReSharper enable InconsistentNaming diff --git a/FineCodeCoverageTests/FCCEngine_Tests.cs b/FineCodeCoverageTests/FCCEngine_Tests.cs index 283b6c77..f8f84d36 100644 --- a/FineCodeCoverageTests/FCCEngine_Tests.cs +++ b/FineCodeCoverageTests/FCCEngine_Tests.cs @@ -35,11 +35,6 @@ public void SetUp() updatedMarginTags = true; }; - - fccEngine.UpdateOutputWindow += (UpdateOutputWindowEventArgs e) => - { - htmlContent = e.HtmlContent; - }; } [Test] @@ -104,7 +99,6 @@ public class FCCEngine_ReloadCoverage_Tests private FCCEngine fccEngine; private List updateMarginTagsEvents; private List> updateMarginTagsCoverageLines; - private List updateOutputWindowEvents; [SetUp] public void SetUp() @@ -114,18 +108,12 @@ public void SetUp() updateMarginTagsEvents = new List(); updateMarginTagsCoverageLines = new List>(); - updateOutputWindowEvents = new List(); fccEngine.UpdateMarginTags += (UpdateMarginTagsEventArgs e) => { updateMarginTagsEvents.Add(e); updateMarginTagsCoverageLines.Add(fccEngine.CoverageLines); }; - - fccEngine.UpdateOutputWindow += (UpdateOutputWindowEventArgs e) => - { - updateOutputWindowEvents.Add(e); - }; } [Test] @@ -411,7 +399,6 @@ private void VerifyLogsReloadCoverageStatus(ReloadCoverageStatus reloadCoverageS private void VerifyClearUIEvents(int eventNumber) { Assert.Null(updateMarginTagsCoverageLines[eventNumber]); - Assert.Null(updateOutputWindowEvents[eventNumber].HtmlContent); } private async Task<(string reportGeneratedHtmlContent, List updatedCoverageLines)> RunToCompletion(bool noCoverageProjects) diff --git a/FineCodeCoverageTests/FineCodeCoverageTests.csproj b/FineCodeCoverageTests/FineCodeCoverageTests.csproj index f2a22e28..e493c8f0 100644 --- a/FineCodeCoverageTests/FineCodeCoverageTests.csproj +++ b/FineCodeCoverageTests/FineCodeCoverageTests.csproj @@ -94,6 +94,7 @@ + diff --git a/FineCodeCoverageTests/ScriptManager_Tests.cs b/FineCodeCoverageTests/ScriptManager_Tests.cs index ca30df2a..af62ff81 100644 --- a/FineCodeCoverageTests/ScriptManager_Tests.cs +++ b/FineCodeCoverageTests/ScriptManager_Tests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine; using FineCodeCoverage.Output; using Moq; @@ -11,13 +12,15 @@ public class ScriptManager_When_Called_Back_Window_External private ScriptManager scriptManager; private Mock sourceFileOpener; private Mock mockProcess; + private Mock mockEventAggregator; [SetUp] public void SetUp() { sourceFileOpener = new Mock(); mockProcess = new Mock(); - scriptManager = new ScriptManager(sourceFileOpener.Object, mockProcess.Object); + mockEventAggregator = new Mock(); + scriptManager = new ScriptManager(sourceFileOpener.Object, mockProcess.Object, mockEventAggregator.Object); } [Test] @@ -42,12 +45,10 @@ public void RateAndReview_Should_Open_Market_Place_Rate_And_Review() } [Test] - public void DocumentFocused_Should_Call_The_Focus_Callback() + public void DocumentFocused_Should_Send_Message() { - var calledCallback = false; - scriptManager.FocusCallback = () => calledCallback = true; scriptManager.DocumentFocused(); - Assert.True(calledCallback); + mockEventAggregator.Verify(e => e.SendMessage(It.IsAny(), null)); } [Test] diff --git a/FineCodeCoverageTests/packages.config b/FineCodeCoverageTests/packages.config index 80059f12..6cd7433d 100644 --- a/FineCodeCoverageTests/packages.config +++ b/FineCodeCoverageTests/packages.config @@ -3,6 +3,7 @@ + diff --git a/SharedProject/Core/CoverageUIEvents.cs b/SharedProject/Core/CoverageUIEvents.cs index add113c1..38a00bc9 100644 --- a/SharedProject/Core/CoverageUIEvents.cs +++ b/SharedProject/Core/CoverageUIEvents.cs @@ -5,11 +5,5 @@ namespace FineCodeCoverage.Engine internal class UpdateMarginTagsEventArgs : EventArgs { } - internal class UpdateOutputWindowEventArgs : EventArgs - { - public string HtmlContent { get; set; } - } - internal delegate void UpdateMarginTagsDelegate(UpdateMarginTagsEventArgs e); - internal delegate void UpdateOutputWindowDelegate(UpdateOutputWindowEventArgs e); } diff --git a/SharedProject/Core/FCCEngine.cs b/SharedProject/Core/FCCEngine.cs index 36dbc3c1..b5e1610b 100644 --- a/SharedProject/Core/FCCEngine.cs +++ b/SharedProject/Core/FCCEngine.cs @@ -3,7 +3,6 @@ using System.ComponentModel.Composition; using System.Linq; using System.Threading; -using System.Windows; using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine.Cobertura; using FineCodeCoverage.Engine.Model; @@ -25,33 +24,10 @@ internal class FCCEngine : IFCCEngine private CancellationTokenSource cancellationTokenSource; public event UpdateMarginTagsDelegate UpdateMarginTags; - public event UpdateOutputWindowDelegate UpdateOutputWindow; public string AppDataFolderPath { get; private set; } public List CoverageLines { get; internal set; } - private DpiScale dpiScale; - public DpiScale Dpi - { - get => dpiScale; - set - { - reportGeneratorUtil.DpiScale = value; - dpiScale = value; - UpdateReportWithDpiFontChanges(); - - } - } - private FontDetails environmentFontDetails; - public FontDetails EnvironmentFontDetails { - get => environmentFontDetails; - set { - environmentFontDetails = value; - reportGeneratorUtil.EnvironmentFontDetails = value; - UpdateReportWithDpiFontChanges(); - } - } - private readonly ICoverageUtilManager coverageUtilManager; private readonly ICoberturaUtil coberturaUtil; private readonly IMsTestPlatformUtil msTestPlatformUtil; @@ -64,7 +40,7 @@ public FontDetails EnvironmentFontDetails { private readonly ICoverageToolOutputManager coverageOutputManager; internal System.Threading.Tasks.Task reloadCoverageTask; private ISolutionEvents solutionEvents; // keep alive - private bool hasGeneratedReport; + private readonly IEventAggregator eventAggregator; [ImportingConstructor] public FCCEngine( @@ -77,10 +53,12 @@ public FCCEngine( IAppDataFolder appDataFolder, ICoverageToolOutputManager coverageOutputManager, ISolutionEvents solutionEvents, - IAppOptionsProvider appOptionsProvider + IAppOptionsProvider appOptionsProvider, + IEventAggregator eventAggregator ) { this.solutionEvents = solutionEvents; + this.eventAggregator = eventAggregator; solutionEvents.AfterClosing += (s,args) => ClearOutputWindow(false); appOptionsProvider.OptionsChanged += (appOptions) => { @@ -132,14 +110,6 @@ private void ClearOutputWindow(bool withHistory) RaiseUpdateOutputWindow(reportGeneratorUtil.BlankReport(withHistory)); } - private void UpdateReportWithDpiFontChanges() - { - if (hasGeneratedReport) - { - reportGeneratorUtil.UpdateReportWithDpiFontChanges(); - } - } - public void StopCoverage() { if (cancellationTokenSource != null) @@ -209,9 +179,7 @@ await coverageProject.StepAsync("Run Coverage Tool", async (project) => private void RaiseUpdateOutputWindow(string reportHtml) { - UpdateOutputWindowEventArgs updateOutputWindowEventArgs = new UpdateOutputWindowEventArgs { HtmlContent = reportHtml}; - UpdateOutputWindow?.Invoke(updateOutputWindowEventArgs); - hasGeneratedReport = true; + eventAggregator.SendMessage(new NewReportMessage { Report = reportHtml }); } private void UpdateUI(List coverageLines, string reportHtml) @@ -362,11 +330,6 @@ public void ReloadCoverage(Func>> coverageRequestCallback); - DpiScale Dpi { get; set; } - void ClearUI(); List CoverageLines { get; } - FontDetails EnvironmentFontDetails { get; set; } - - void ReadyForReport(); } } \ No newline at end of file diff --git a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs index 69323992..0d0825d0 100644 --- a/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs +++ b/SharedProject/Core/ReportGenerator/ReportGeneratorUtil.cs @@ -19,20 +19,14 @@ namespace FineCodeCoverage.Engine.ReportGenerator { - interface IReportGeneratorUtil { - DpiScale DpiScale { get; set; } - void Initialize(string appDataFolder); string ProcessUnifiedHtml(string htmlForProcessing,string reportOutputFolder); Task GenerateAsync(IEnumerable coverOutputFiles,string reportOutputFolder, bool throwError = false); string BlankReport(bool withHistory); System.Threading.Tasks.Task LogCoverageProcessAsync(string message); System.Threading.Tasks.Task EndOfCoverageRunAsync(); - void UpdateReportWithDpiFontChanges(); - - FontDetails EnvironmentFontDetails { get; set; } } internal class ReportGeneratorResult @@ -43,7 +37,9 @@ internal class ReportGeneratorResult } [Export(typeof(IReportGeneratorUtil))] - internal partial class ReportGeneratorUtil : IReportGeneratorUtil + internal partial class ReportGeneratorUtil : + IReportGeneratorUtil, + IListener, IListener, IListener { private readonly IAssemblyUtil assemblyUtil; private readonly IProcessUtil processUtil; @@ -55,6 +51,7 @@ internal partial class ReportGeneratorUtil : IReportGeneratorUtil private readonly IAppOptionsProvider appOptionsProvider; private readonly IResourceProvider resourceProvider; private readonly IShowFCCOutputPane showFCCOutputPane; + private readonly IEventAggregator eventAggregator; private const string zipPrefix = "reportGenerator"; private const string zipDirectoryName = "reportGenerator"; @@ -69,7 +66,11 @@ internal partial class ReportGeneratorUtil : IReportGeneratorUtil private readonly Base64ReportImage downInactiveBase64ReportImage = new Base64ReportImage(".icon-down-dir", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB3aWR0aD0iMTc5MiIgaGVpZ2h0PSIxNzkyIiB2aWV3Qm94PSIwIDAgMTc5MiAxNzkyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xNDA4IDcwNHEwIDI2LTE5IDQ1bC00NDggNDQ4cS0xOSAxOS00NSAxOXQtNDUtMTlsLTQ0OC00NDhxLTE5LTE5LTE5LTQ1dDE5LTQ1IDQ1LTE5aDg5NnEyNiAwIDQ1IDE5dDE5IDQ1eiIvPjwvc3ZnPg=="); private readonly Base64ReportImage upActiveBase64ReportImage = new Base64ReportImage(".icon-up-dir_active", "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjxzdmcgd2lkdGg9IjE3OTIiIGhlaWdodD0iMTc5MiIgdmlld0JveD0iMCAwIDE3OTIgMTc5MiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsPSIjMDA3OEQ0IiBkPSJNMTQwOCAxMjE2cTAgMjYtMTkgNDV0LTQ1IDE5aC04OTZxLTI2IDAtNDUtMTl0LTE5LTQ1IDE5LTQ1bDQ0OC00NDhxMTktMTkgNDUtMTl0NDUgMTlsNDQ4IDQ0OHExOSAxOSAxOSA0NXoiLz48L3N2Zz4="); private readonly IScriptManager scriptManager; + private DpiScale dpiScale; + private FontDetails environmentFontDetails; private string previousFontSizeName; + private string unprocessedReport; + private string previousReportOutputFolder; private IReportColours reportColours; private JsThemeStyling jsReportColours; private IReportColours ReportColours @@ -85,11 +86,9 @@ private IReportColours ReportColours private List logs = new List(); public string ReportGeneratorExePath { get; private set; } - public DpiScale DpiScale { get; set; } - public FontDetails EnvironmentFontDetails { get; set; } - private string FontSize => $"{EnvironmentFontDetails.Size * DpiScale.DpiScaleX}px"; - private string FontName => EnvironmentFontDetails.Family.Source; + private string FontSize => $"{environmentFontDetails.Size * dpiScale.DpiScaleX}px"; + private string FontName => environmentFontDetails.Family.Source; [ImportingConstructor] public ReportGeneratorUtil( @@ -103,7 +102,8 @@ public ReportGeneratorUtil( IReportColoursProvider reportColoursProvider, IScriptManager scriptManager, IResourceProvider resourceProvider, - IShowFCCOutputPane showFCCOutputPane + IShowFCCOutputPane showFCCOutputPane, + IEventAggregator eventAggregator ) { this.fileUtil = fileUtil; @@ -118,6 +118,8 @@ IShowFCCOutputPane showFCCOutputPane this.scriptManager = scriptManager; this.resourceProvider = resourceProvider; this.showFCCOutputPane = showFCCOutputPane; + this.eventAggregator = eventAggregator; + this.eventAggregator.AddListener(this); scriptManager.ClearFCCWindowLogsEvent += ScriptManager_ClearFCCWindowLogsEvent; scriptManager.ShowFCCOutputPaneEvent += ScriptManager_ShowFCCOutputPaneEvent; } @@ -886,6 +888,8 @@ private string GetFontNameSize() public string ProcessUnifiedHtml(string htmlForProcessing, string reportOutputFolder) { previousFontSizeName = GetFontNameSize(); + unprocessedReport = htmlForProcessing; + previousReportOutputFolder = reportOutputFolder; var previousLogMessages = $"[{string.Join(",",logs.Select(l => $"'{l}'"))}]"; var appOptions = appOptionsProvider.Get(); var namespacedClasses = appOptions.NamespacedClasses; @@ -1570,17 +1574,7 @@ private void HideRowsFromOverviewTable(HtmlDocument doc) private void ReportColoursProvider_ColoursChanged(object sender, IReportColours reportColours) { - var jsThemeStyling = reportColours.Convert(); - - var coverageTableActiveSortColour = reportColours.CoverageTableActiveSortColour.ToJsColour(); - var coverageTableExpandCollapseIconColour = reportColours.CoverageTableExpandCollapseIconColour.ToJsColour(); - jsThemeStyling.DownActiveBase64 = downActiveBase64ReportImage.Base64FromColour(coverageTableActiveSortColour); - jsThemeStyling.UpActiveBase64 = upActiveBase64ReportImage.Base64FromColour(coverageTableActiveSortColour); - jsThemeStyling.DownInactiveBase64 = downInactiveBase64ReportImage.Base64FromColour(reportColours.CoverageTableInactiveSortColour.ToJsColour()); - jsThemeStyling.MinusBase64 = minusBase64ReportImage.Base64FromColour(coverageTableExpandCollapseIconColour); - jsThemeStyling.PlusBase64 = plusBase64ReportImage.Base64FromColour(coverageTableExpandCollapseIconColour); - - scriptManager.InvokeScript(ThemeChangedJSFunctionName, jsThemeStyling); + ReprocessReport(); } public string BlankReport(bool withHistory) @@ -1595,28 +1589,47 @@ public string BlankReport(bool withHistory) public async System.Threading.Tasks.Task LogCoverageProcessAsync(string message) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - scriptManager.InvokeScript(CoverageLogJSFunctionName, message); + eventAggregator.SendMessage(new InvokeScriptMessage(CoverageLogJSFunctionName, message)); logs.Add(message); } public async System.Threading.Tasks.Task EndOfCoverageRunAsync() { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - scriptManager.InvokeScript(ShowFCCWorkingJSFunctionName, false); - } - - public void UpdateReportWithDpiFontChanges() + eventAggregator.SendMessage(new InvokeScriptMessage(ShowFCCWorkingJSFunctionName, false)); + } + + public void UpdateReportWithDpiFontChanges() { - if (previousFontSizeName != GetFontNameSize()) + if (unprocessedReport !=null && previousFontSizeName != GetFontNameSize()) { - scriptManager.InvokeScript(FontChangedJSFunctionName, $"{FontName}:{FontSize}"); + ReprocessReport(); } } - //private string ToJsColour(System.Drawing.Color colour) - //{ - // return $"rgba({colour.R},{colour.G},{colour.B},{colour.A})"; - //} + private void ReprocessReport() + { + var newReport = ProcessUnifiedHtml(unprocessedReport, previousReportOutputFolder); + eventAggregator.SendMessage(new NewReportMessage { Report = newReport }); + } - } + public void Handle(EnvironmentFontDetailsChangedMessage message) + { + environmentFontDetails = message.FontDetails; + UpdateReportWithDpiFontChanges(); + } + + public void Handle(DpiChangedMessage message) + { + dpiScale = message.DpiScale; + UpdateReportWithDpiFontChanges(); + } + + public void Handle(ReadyForReportMessage message) + { + var newReport = BlankReport(false); + eventAggregator.SendMessage(new ObjectForScriptingMessage { ObjectForScripting = scriptManager }); + eventAggregator.SendMessage(new NewReportMessage { Report = newReport }); + } + } } diff --git a/SharedProject/Core/Utilities/EventAggregator.cs b/SharedProject/Core/Utilities/EventAggregator.cs new file mode 100644 index 00000000..915d1d4c --- /dev/null +++ b/SharedProject/Core/Utilities/EventAggregator.cs @@ -0,0 +1,567 @@ +// ReSharper disable InconsistentNaming +namespace FineCodeCoverage.Core.Utilities +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using System.Linq; + using System.Reflection; + +/* + * + * License: + * + * Microsoft Public License (MS-PL) + * + * This license governs use of the accompanying software. If you use the software, you + * accept this license. If you do not accept the license, do not use the software. + * + * 1. Definitions + * The terms "reproduce," "reproduction," "derivative works," and "distribution" have the + * same meaning here as under U.S. copyright law. + * A "contribution" is the original software, or any additions or changes to the software. + * A "contributor" is any person that distributes its contribution under this license. + * "Licensed patents" are a contributor's patent claims that read directly on its contribution. + * + * 2. Grant of Rights + * (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + * (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + * + * 3. Conditions and Limitations + * (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + * (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. + * (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. + * (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. + * (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. + * + * Little bit of history: + * EventAggregator origins based on work from StatLight's EventAggregator. Which + * is based on original work by Jermey Miller's EventAggregator in StoryTeller + * with some concepts pulled from Rob Eisenberg in caliburnmicro. + * + * TODO: + * - Possibly provide well defined initial thread marshalling actions (depending on platform (WinForm, WPF, Silverlight, WP7???) + * - Document the public API better. + * + * Thanks to: + * - Jermey Miller - initial implementation + * - Rob Eisenberg - pulled some ideas from the caliburn micro event aggregator + * - Jake Ginnivan - https://github.com/JakeGinnivan - thanks for the pull requests + * + */ + +/// +/// Specifies a class that would like to receive particular messages. +/// +/// The type of message object to subscribe to. +#if WINDOWS_PHONE + public interface IListener +#else +public interface IListener +#endif +{ + /// + /// This will be called every time a TMessage is published through the event aggregator + /// + void Handle(TMessage message); +} + +/// +/// Provides a way to add and remove a listener object from the EventAggregator +/// +public interface IEventSubscriptionManager +{ + /// + /// Adds the given listener object to the EventAggregator. + /// + /// Object that should be implementing IListener(of T's), this overload is used when your listeners to multiple message types + /// determines if the EventAggregator should hold a weak or strong reference to the listener object. If null it will use the Config level option unless overriden by the parameter. + /// Returns the current IEventSubscriptionManager to allow for easy fluent additions. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + IEventSubscriptionManager AddListener(object listener, bool? holdStrongReference = null); + + /// + /// Adds the given listener object to the EventAggregator. + /// + /// Listener Message type + /// + /// determines if the EventAggregator should hold a weak or strong reference to the listener object. If null it will use the Config level option unless overriden by the parameter. + /// Returns the current IEventSubscriptionManager to allow for easy fluent additions. + IEventSubscriptionManager AddListener(IListener listener, bool? holdStrongReference = null); + + /// + /// Removes the listener object from the EventAggregator + /// + /// The object to be removed + /// Returnes the current IEventSubscriptionManager for fluent removals. + IEventSubscriptionManager RemoveListener(object listener); +} + +public interface IEventPublisher +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + void SendMessage(TMessage message, Action marshal = null); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter"), + System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + void SendMessage(Action marshal = null) + where TMessage : new(); +} + +public interface IEventAggregator : IEventPublisher, IEventSubscriptionManager +{ +} +[Export(typeof(IEventAggregator))] +public class EventAggregator : IEventAggregator +{ + private readonly ListenerWrapperCollection _listeners; + private readonly Config _config; + + public EventAggregator() + : this(new Config()) + { + } + + public EventAggregator(Config config) + { + _config = config; + _listeners = new ListenerWrapperCollection(); + } + + /// + /// This will send the message to each IListener that is subscribing to TMessage. + /// + /// The type of message being sent + /// The message instance + /// You can optionally override how the message publication action is marshalled + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] + public void SendMessage(TMessage message, Action marshal = null) + { + if (marshal == null) + marshal = _config.DefaultThreadMarshaler; + + Call>(message, marshal); + } + + /// + /// This will create a new default instance of TMessage and send the message to each IListener that is subscribing to TMessage. + /// + /// The type of message being sent + /// You can optionally override how the message publication action is marshalled + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed"), + System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", + "CA1004:GenericMethodsShouldProvideTypeParameter")] + public void SendMessage(Action marshal = null) + where TMessage : new() + { + SendMessage(new TMessage(), marshal); + } + + private void Call(object message, Action marshaller) + where TListener : class + { + int listenerCalledCount = 0; + marshaller(() => + { + foreach (ListenerWrapper o in _listeners.Where(o => o.Handles() || o.HandlesMessage(message))) + { + bool wasThisOneCalled; + o.TryHandle(message, out wasThisOneCalled); + if (wasThisOneCalled) + listenerCalledCount++; + } + }); + + var wasAnyListenerCalled = listenerCalledCount > 0; + + if (!wasAnyListenerCalled) + { + _config.OnMessageNotPublishedBecauseZeroListeners(message); + } + } + + public IEventSubscriptionManager AddListener(object listener) + { + return AddListener(listener, null); + } + + public IEventSubscriptionManager AddListener(object listener, bool? holdStrongReference) + { + if (listener == null) throw new ArgumentNullException("listener"); + + bool holdRef = _config.HoldReferences; + if (holdStrongReference.HasValue) + holdRef = holdStrongReference.Value; + bool supportMessageInheritance = _config.SupportMessageInheritance; + _listeners.AddListener(listener, holdRef, supportMessageInheritance); + + return this; + } + + public IEventSubscriptionManager AddListener(IListener listener, bool? holdStrongReference) + { + AddListener((object)listener, holdStrongReference); + + return this; + } + + public IEventSubscriptionManager RemoveListener(object listener) + { + _listeners.RemoveListener(listener); + return this; + } + + /// + /// Wrapper collection of ListenerWrappers to manage things like + /// threadsafe manipulation to the collection, and convenience + /// methods to configure the collection + /// + private class ListenerWrapperCollection : IEnumerable + { + private readonly List _listeners = new List(); + private readonly object _sync = new object(); + + public void RemoveListener(object listener) + { + ListenerWrapper listenerWrapper; + lock (_sync) + if (TryGetListenerWrapperByListener(listener, out listenerWrapper)) + _listeners.Remove(listenerWrapper); + } + + private void RemoveListenerWrapper(ListenerWrapper listenerWrapper) + { + lock (_sync) + _listeners.Remove(listenerWrapper); + } + + public IEnumerator GetEnumerator() + { + lock (_sync) + return _listeners.ToList().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private bool ContainsListener(object listener) + { + ListenerWrapper listenerWrapper; + return TryGetListenerWrapperByListener(listener, out listenerWrapper); + } + + private bool TryGetListenerWrapperByListener(object listener, out ListenerWrapper listenerWrapper) + { + lock (_sync) + listenerWrapper = _listeners.SingleOrDefault(x => x.ListenerInstance == listener); + + return listenerWrapper != null; + } + + public void AddListener(object listener, bool holdStrongReference, bool supportMessageInheritance) + { + lock (_sync) + { + + if (ContainsListener(listener)) + return; + + var listenerWrapper = new ListenerWrapper(listener, RemoveListenerWrapper, holdStrongReference, supportMessageInheritance); + if (listenerWrapper.Count == 0) + throw new ArgumentException("IListener is not implemented", "listener"); + _listeners.Add(listenerWrapper); + } + } + } + + #region IReference + + private interface IReference + { + object Target { get; } + } + + private class WeakReferenceImpl : IReference + { + private readonly WeakReference _reference; + + public WeakReferenceImpl(object listener) + { + _reference = new WeakReference(listener); + } + + public object Target + { + get { return _reference.Target; } + } + } + + private class StrongReferenceImpl : IReference + { + private readonly object _target; + + public StrongReferenceImpl(object target) + { + _target = target; + } + + public object Target + { + get { return _target; } + } + } + + #endregion + + private class ListenerWrapper + { + private const string HandleMethodName = "Handle"; + private readonly Action _onRemoveCallback; + private readonly List _handlers = new List(); + private readonly IReference _reference; + + public ListenerWrapper(object listener, Action onRemoveCallback, bool holdReferences, bool supportMessageInheritance) + { + _onRemoveCallback = onRemoveCallback; + + if (holdReferences) + _reference = new StrongReferenceImpl(listener); + else + _reference = new WeakReferenceImpl(listener); + + var listenerInterfaces = TypeHelper.GetBaseInterfaceType(listener.GetType()) + .Where(w => TypeHelper.DirectlyClosesGeneric(w, typeof(IListener<>))); + + foreach (var listenerInterface in listenerInterfaces) + { + var messageType = TypeHelper.GetFirstGenericType(listenerInterface); + var handleMethod = TypeHelper.GetMethod(listenerInterface, HandleMethodName); + + HandleMethodWrapper handler = new HandleMethodWrapper(handleMethod, listenerInterface, messageType, supportMessageInheritance); + _handlers.Add(handler); + } + } + + public object ListenerInstance + { + get { return _reference.Target; } + } + + public bool Handles() where TListener : class + { + return _handlers.Aggregate(false, (current, handler) => current | handler.Handles()); + } + + public bool HandlesMessage(object message) + { + return message != null && _handlers.Aggregate(false, (current, handler) => current | handler.HandlesMessage(message)); + } + + public void TryHandle(object message, out bool wasHandled) + where TListener : class + { + var target = _reference.Target; + wasHandled = false; + if (target == null) + { + _onRemoveCallback(this); + return; + } + + foreach (var handler in _handlers) + { + bool thisOneHandled = false; + handler.TryHandle(target, message, out thisOneHandled); + wasHandled |= thisOneHandled; + } + } + + public int Count + { + get { return _handlers.Count; } + } + } + + private class HandleMethodWrapper + { + private readonly Type _listenerInterface; + private readonly Type _messageType; + private readonly MethodInfo _handlerMethod; + private readonly bool _supportMessageInheritance; + private readonly Dictionary supportedMessageTypes = new Dictionary(); + + public HandleMethodWrapper(MethodInfo handlerMethod, Type listenerInterface, Type messageType, bool supportMessageInheritance) + { + _handlerMethod = handlerMethod; + _listenerInterface = listenerInterface; + _messageType = messageType; + _supportMessageInheritance = supportMessageInheritance; + supportedMessageTypes[messageType] = true; + } + + public bool Handles() where TListener : class + { + return _listenerInterface == typeof(TListener); + } + + public bool HandlesMessage(object message) + { + if (message == null) + { + return false; + } + + bool handled; + Type messageType = message.GetType(); + bool previousMessageType = supportedMessageTypes.TryGetValue(messageType, out handled); + if (!previousMessageType && _supportMessageInheritance) + { + handled = TypeHelper.IsAssignableFrom(_messageType, messageType); + supportedMessageTypes[messageType] = handled; + } + return handled; + } + + public void TryHandle(object target, object message, out bool wasHandled) + where TListener : class + { + wasHandled = false; + if (target == null) + { + return; + } + + if (!Handles() && !HandlesMessage(message)) return; + + _handlerMethod.Invoke(target, new[] { message }); + wasHandled = true; + } + } + + internal static class TypeHelper + { + internal static IEnumerable GetBaseInterfaceType(Type type) + { + if (type == null) + return new Type[0]; + +#if NETFX_CORE + var interfaces = type.GetTypeInfo().ImplementedInterfaces.ToList(); +#else + var interfaces = type.GetInterfaces().ToList(); +#endif + + foreach (var @interface in interfaces.ToArray()) + { + interfaces.AddRange(GetBaseInterfaceType(@interface)); + } + +#if NETFX_CORE + if (type.GetTypeInfo().IsInterface) +#else + if (type.IsInterface) +#endif + { + interfaces.Add(type); + } + + return interfaces.Distinct(); + } + + internal static bool DirectlyClosesGeneric(Type type, Type openType) + { + if (type == null) + return false; +#if NETFX_CORE + if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == openType) +#else + if (type.IsGenericType && type.GetGenericTypeDefinition() == openType) +#endif + { + return true; + } + + return false; + } + + internal static Type GetFirstGenericType() where T : class + { + return GetFirstGenericType(typeof(T)); + } + + internal static Type GetFirstGenericType(Type type) + { +#if NETFX_CORE + var messageType = type.GetTypeInfo().GenericTypeArguments.First(); +#else + var messageType = type.GetGenericArguments().First(); +#endif + return messageType; + } + + internal static MethodInfo GetMethod(Type type, string methodName) + { +#if NETFX_CORE + var typeInfo = type.GetTypeInfo(); + var handleMethod = typeInfo.GetDeclaredMethod(methodName); +#else + var handleMethod = type.GetMethod(methodName); + +#endif + return handleMethod; + } + + internal static bool IsAssignableFrom(Type type, Type specifiedType) + { +#if NETFX_CORE + return type.GetTypeInfo().IsAssignableFrom(specifiedType.GetTypeInfo()); +#else + return type.IsAssignableFrom(specifiedType); +#endif + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] + public class Config + { + private Action _onMessageNotPublishedBecauseZeroListeners = msg => + { + /* TODO: possibly Trace message?*/ + }; + + public Action OnMessageNotPublishedBecauseZeroListeners + { + get { return _onMessageNotPublishedBecauseZeroListeners; } + set { _onMessageNotPublishedBecauseZeroListeners = value; } + } + + private Action _defaultThreadMarshaler = action => action(); + + public Action DefaultThreadMarshaler + { + get { return _defaultThreadMarshaler; } + set { _defaultThreadMarshaler = value; } + } + + /// + /// If true instructs the EventAggregator to hold onto a reference to all listener objects. You will then have to explicitly remove them from the EventAggrator. + /// If false then a WeakReference is used and the garbage collector can remove the listener when not in scope any longer. + /// + public bool HoldReferences { get; set; } + + /// + /// If true then EventAggregator will support registering listeners for base messages. + /// If false then EventAggregator will only match the message type to the listener. + /// + public bool SupportMessageInheritance { get; set; } + } +} + + +} + +// ReSharper enable InconsistentNaming + diff --git a/SharedProject/Output/DpiChangedMessage.cs b/SharedProject/Output/DpiChangedMessage.cs new file mode 100644 index 00000000..b8969555 --- /dev/null +++ b/SharedProject/Output/DpiChangedMessage.cs @@ -0,0 +1,10 @@ +using System.Windows; + +namespace FineCodeCoverage.Output +{ + internal class DpiChangedMessage + { + public DpiScale DpiScale { get; set; } + } + +} diff --git a/SharedProject/Output/EnvironmentFont.cs b/SharedProject/Output/EnvironmentFont.cs new file mode 100644 index 00000000..35f8dc45 --- /dev/null +++ b/SharedProject/Output/EnvironmentFont.cs @@ -0,0 +1,64 @@ +using Microsoft.VisualStudio.Shell; +using System; +using System.Windows; +using System.Windows.Media; + +namespace FineCodeCoverage.Output +{ + public class FontDetails + { + public FontDetails(double size, FontFamily fontFamily) + { + Size = size; + Family = fontFamily; + } + public double Size { get; } + + public FontFamily Family { get; } + } + + public class EnvironmentFont : DependencyObject + { + + private static DependencyProperty EnvironmentFontSizeProperty; + + private static DependencyProperty EnvironmentFontFamilyProperty; + + private double Size { get; set; } + + private FontFamily Family { get; set; } + + + public event EventHandler Changed; + + public void Initialize(FrameworkElement frameworkElement) + { + RegisterDependencyProperties(frameworkElement.GetType()); + frameworkElement.SetResourceReference(EnvironmentFontSizeProperty, VsFonts.EnvironmentFontSizeKey); + frameworkElement.SetResourceReference(EnvironmentFontFamilyProperty, VsFonts.EnvironmentFontFamilyKey); + } + + private void RegisterDependencyProperties(Type controlType) + { + EnvironmentFontSizeProperty = DependencyProperty.Register("EnvironmentFontSize", typeof(double), controlType, new PropertyMetadata((obj, args) => + { + Size = (double)args.NewValue; + ValueChanged(); + })); + + EnvironmentFontFamilyProperty = DependencyProperty.Register("EnvironmentFontFamily", typeof(FontFamily), controlType, new PropertyMetadata((obj, args) => + { + Family = (FontFamily)args.NewValue; + ValueChanged(); + })); + } + + private void ValueChanged() + { + if (Family != null && Size != default) + { + Changed?.Invoke(this, new FontDetails(Size, Family)); + } + } + } +} diff --git a/SharedProject/Output/EnvironmentFontDetailsChangedMessage.cs b/SharedProject/Output/EnvironmentFontDetailsChangedMessage.cs new file mode 100644 index 00000000..9eb0e7f9 --- /dev/null +++ b/SharedProject/Output/EnvironmentFontDetailsChangedMessage.cs @@ -0,0 +1,8 @@ +namespace FineCodeCoverage.Output +{ + internal class EnvironmentFontDetailsChangedMessage + { + public FontDetails FontDetails { get; set; } + } + +} diff --git a/SharedProject/Output/InvokeScriptMessage.cs b/SharedProject/Output/InvokeScriptMessage.cs new file mode 100644 index 00000000..95412b8f --- /dev/null +++ b/SharedProject/Output/InvokeScriptMessage.cs @@ -0,0 +1,21 @@ +namespace FineCodeCoverage.Output +{ + internal class InvokeScriptMessage + { + public string ScriptName { get; set; } + public object[] Arguments { get; set; } + + public InvokeScriptMessage(string scriptName) + { + ScriptName = scriptName; + } + + public InvokeScriptMessage(string scriptName, object argument) : this(scriptName, new object[] { argument }) + { } + + public InvokeScriptMessage(string scriptName, object[] arguments) : this(scriptName) + { + Arguments = arguments; + } + } +} diff --git a/SharedProject/Output/NewReportMessage.cs b/SharedProject/Output/NewReportMessage.cs new file mode 100644 index 00000000..c4f4be3b --- /dev/null +++ b/SharedProject/Output/NewReportMessage.cs @@ -0,0 +1,8 @@ +namespace FineCodeCoverage.Output +{ + internal class NewReportMessage + { + public string Report { get; set; } + } + +} diff --git a/SharedProject/Output/ObjectForScriptingMessage.cs b/SharedProject/Output/ObjectForScriptingMessage.cs new file mode 100644 index 00000000..d9dd3108 --- /dev/null +++ b/SharedProject/Output/ObjectForScriptingMessage.cs @@ -0,0 +1,7 @@ +namespace FineCodeCoverage.Output +{ + internal class ObjectForScriptingMessage + { + public object ObjectForScripting { get; set; } + } +} diff --git a/SharedProject/Output/OutputToolWindow.cs b/SharedProject/Output/OutputToolWindow.cs index 2a1efb63..5bac1b80 100644 --- a/SharedProject/Output/OutputToolWindow.cs +++ b/SharedProject/Output/OutputToolWindow.cs @@ -6,6 +6,7 @@ using Microsoft.VisualStudio.Shell; using System.Runtime.InteropServices; using Microsoft.VisualStudio.Shell.Interop; +using FineCodeCoverage.Core.Utilities; namespace FineCodeCoverage.Output { @@ -21,7 +22,7 @@ namespace FineCodeCoverage.Output /// /// [Guid("320fd13f-632f-4b16-9527-a1adfe555f6c")] - internal class OutputToolWindow : ToolWindowPane + internal class OutputToolWindow : ToolWindowPane, IListener { /// /// Initializes a new instance of the class. @@ -40,14 +41,7 @@ private void Initialize(OutputToolWindowContext context) { //to see if OutputToolWindow can be internal ( and thus IScriptManager ) Caption = Vsix.Name; - context.ScriptManager.FocusCallback = () => - { - ThreadHelper.JoinableTaskFactory.Run(async () => - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - (this.Frame as IVsWindowFrame).Show(); - }); - }; + context.EventAggregator.AddListener(this); // This is the user control hosted by the tool window; Note that, even if this class implements IDisposable, // we are not calling Dispose on this object. This is because ToolWindowPane calls Dispose on @@ -56,7 +50,7 @@ private void Initialize(OutputToolWindowContext context) try { AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; - Content = new OutputToolWindowControl(context.ScriptManager, context.FccEngine); + Content = new OutputToolWindowControl(context.EventAggregator); } finally { @@ -110,5 +104,14 @@ private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs a return null; } - } + + public void Handle(ReportFocusedMessage message) + { + ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + (this.Frame as IVsWindowFrame).Show(); + }); + } + } } diff --git a/SharedProject/Output/OutputToolWindowContext.cs b/SharedProject/Output/OutputToolWindowContext.cs index 3ef1529c..24d71b80 100644 --- a/SharedProject/Output/OutputToolWindowContext.cs +++ b/SharedProject/Output/OutputToolWindowContext.cs @@ -1,10 +1,9 @@ -using FineCodeCoverage.Engine; +using FineCodeCoverage.Core.Utilities; namespace FineCodeCoverage.Output { internal class OutputToolWindowContext { - public ScriptManager ScriptManager { get; set; } - public IFCCEngine FccEngine { get; set; } + public IEventAggregator EventAggregator { get; set; } } } diff --git a/SharedProject/Output/OutputToolWindowControl.xaml.cs b/SharedProject/Output/OutputToolWindowControl.xaml.cs index e71d8945..07b034c2 100644 --- a/SharedProject/Output/OutputToolWindowControl.xaml.cs +++ b/SharedProject/Output/OutputToolWindowControl.xaml.cs @@ -1,127 +1,51 @@ -using EnvDTE; -using System.Windows; -using FineCodeCoverage.Engine; +using System.Windows; using System.Windows.Controls; using Microsoft.VisualStudio.Shell; -using Microsoft; -using System; using System.Windows.Media; +using FineCodeCoverage.Core.Utilities; namespace FineCodeCoverage.Output { - public class FontDetails - { - public FontDetails(double size, FontFamily fontFamily) - { - Size = size; - Family = fontFamily; - } - public double Size { get; } - - public FontFamily Family { get; } - } - - public class EnvironmentFont : DependencyObject - { - - private static DependencyProperty EnvironmentFontSizeProperty; - - private static DependencyProperty EnvironmentFontFamilyProperty; - - private double Size { get; set; } - - private FontFamily Family { get; set; } - - - public event EventHandler Changed; - - public void Initialize(FrameworkElement frameworkElement) - { - RegisterDependencyProperties(frameworkElement.GetType()); - frameworkElement.SetResourceReference(EnvironmentFontSizeProperty, VsFonts.EnvironmentFontSizeKey); - frameworkElement.SetResourceReference(EnvironmentFontFamilyProperty, VsFonts.EnvironmentFontFamilyKey); - } - - private void RegisterDependencyProperties(Type controlType) - { - EnvironmentFontSizeProperty = DependencyProperty.Register("EnvironmentFontSize", typeof(double), controlType, new PropertyMetadata((obj, args) => - { - Size = (double)args.NewValue; - ValueChanged(); - })); - - EnvironmentFontFamilyProperty = DependencyProperty.Register("EnvironmentFontFamily", typeof(FontFamily), controlType, new PropertyMetadata((obj, args) => - { - Family = (FontFamily)args.NewValue; - ValueChanged(); - })); - } - - private void ValueChanged() - { - if (Family != null && Size != default) - { - Changed?.Invoke(this, new FontDetails(Size, Family)); - } - } - } - /// /// Interaction logic for OutputToolWindowControl. /// - internal partial class OutputToolWindowControl : UserControl, IScriptInvoker + internal partial class OutputToolWindowControl : + UserControl, IListener, IListener, IListener { - private readonly IFCCEngine fccEngine; - private bool hasLoaded; + private readonly IEventAggregator eventAggregator; /// /// Initializes a new instance of the class. /// - public OutputToolWindowControl(ScriptManager scriptManager,IFCCEngine fccEngine) + public OutputToolWindowControl(IEventAggregator eventAggregator) { + this.eventAggregator = eventAggregator; InitializeComponent(); - fccEngine.Dpi = VisualTreeHelper.GetDpi(this); + eventAggregator.SendMessage(new DpiChangedMessage { DpiScale = VisualTreeHelper.GetDpi(this) }); var environmentFont = new EnvironmentFont(); environmentFont.Changed += (sender, fontDetails) => { - fccEngine.EnvironmentFontDetails = fontDetails; + eventAggregator.SendMessage(new EnvironmentFontDetailsChangedMessage { FontDetails = fontDetails }); }; environmentFont.Initialize(this); this.Loaded += OutputToolWindowControl_Loaded; - FCCOutputBrowser.ObjectForScripting = scriptManager; - scriptManager.ScriptInvoker = this; - - fccEngine.UpdateOutputWindow += (args) => - { - ThreadHelper.JoinableTaskFactory.Run(async () => - { - await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); - - FCCOutputBrowser.NavigateToString(args.HtmlContent); - }); - }; - - this.fccEngine = fccEngine; + eventAggregator.AddListener(this); + eventAggregator.SendMessage(new ReadyForReportMessage()); } protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) { base.OnDpiChanged(oldDpi, newDpi); - fccEngine.Dpi = newDpi; + eventAggregator.SendMessage(new DpiChangedMessage { DpiScale = newDpi }); } private void OutputToolWindowControl_Loaded(object sender, RoutedEventArgs e) { - if (!hasLoaded) - { - fccEngine.ReadyForReport(); - FCCOutputBrowser.Visibility = Visibility.Visible; - hasLoaded = true; - } + FCCOutputBrowser.Visibility = Visibility.Visible; } - public object InvokeScript(string scriptName, params object[] args) + private void InvokeScript(string scriptName, params object[] args) { if (FCCOutputBrowser.Document != null) { @@ -129,13 +53,32 @@ public object InvokeScript(string scriptName, params object[] args) { // Can use FCCOutputBrowser.IsLoaded but // it is possible for this to be successful when IsLoaded false. - return FCCOutputBrowser.InvokeScript(scriptName, args); + FCCOutputBrowser.InvokeScript(scriptName, args); } catch { - // todo what to do about missed + // missed are not important. Important go through NewReportMessage and NavigateToString } } - return null; } - } + + public void Handle(NewReportMessage message) + { + ThreadHelper.JoinableTaskFactory.Run(async () => + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + FCCOutputBrowser.NavigateToString(message.Report); + }); + } + + public void Handle(InvokeScriptMessage message) + { + InvokeScript(message.ScriptName, message.Arguments); + } + + public void Handle(ObjectForScriptingMessage message) + { + FCCOutputBrowser.ObjectForScripting = message.ObjectForScripting; + } + } } \ No newline at end of file diff --git a/SharedProject/Output/OutputToolWindowPackage.cs b/SharedProject/Output/OutputToolWindowPackage.cs index 6ee5ddea..c3d285d2 100644 --- a/SharedProject/Output/OutputToolWindowPackage.cs +++ b/SharedProject/Output/OutputToolWindowPackage.cs @@ -10,6 +10,7 @@ using EnvDTE80; using Microsoft; using FineCodeCoverage.Engine; +using FineCodeCoverage.Core.Utilities; namespace FineCodeCoverage.Output { @@ -65,8 +66,7 @@ internal static OutputToolWindowContext GetOutputToolWindowContext() { return new OutputToolWindowContext { - FccEngine = componentModel.GetService(), - ScriptManager = componentModel.GetService() + EventAggregator = componentModel.GetService() }; } diff --git a/SharedProject/Output/ReadyForReportMessage.cs b/SharedProject/Output/ReadyForReportMessage.cs new file mode 100644 index 00000000..4db2a8c5 --- /dev/null +++ b/SharedProject/Output/ReadyForReportMessage.cs @@ -0,0 +1,6 @@ +namespace FineCodeCoverage.Output +{ + internal class ReadyForReportMessage + { + } +} diff --git a/SharedProject/Output/ScriptManager.cs b/SharedProject/Output/ScriptManager.cs index 3656b762..b0938380 100644 --- a/SharedProject/Output/ScriptManager.cs +++ b/SharedProject/Output/ScriptManager.cs @@ -1,20 +1,22 @@ using System; using System.ComponentModel.Composition; using System.Runtime.InteropServices; +using FineCodeCoverage.Core.Utilities; using FineCodeCoverage.Engine; namespace FineCodeCoverage.Output { - public interface IScriptInvoker - { - object InvokeScript(string scriptName, params object[] args); - } - public interface IScriptManager : IScriptInvoker + public interface IScriptManager { event EventHandler ClearFCCWindowLogsEvent; event EventHandler ShowFCCOutputPaneEvent; } + public class ReportFocusedMessage + { + + } + [Export] [Export(typeof(IScriptManager))] [ComVisible(true)] // do not change the accessibility - needs to be public class @@ -25,17 +27,17 @@ public class ScriptManager : IScriptManager internal const string marketPlaceRateAndReview = "https://marketplace.visualstudio.com/items?itemName=FortuneNgwenya.FineCodeCoverage&ssr=false#review-details"; private readonly ISourceFileOpener sourceFileOpener; private readonly IProcess process; + private readonly IEventAggregator eventAggregator; internal System.Threading.Tasks.Task openFileTask; public event EventHandler ClearFCCWindowLogsEvent; public event EventHandler ShowFCCOutputPaneEvent; - public IScriptInvoker ScriptInvoker { get; set; } - public Action FocusCallback { get; set; } [ImportingConstructor] - internal ScriptManager(ISourceFileOpener sourceFileOpener, IProcess process) + internal ScriptManager(ISourceFileOpener sourceFileOpener, IProcess process, IEventAggregator eventAggregator) { this.sourceFileOpener = sourceFileOpener; this.process = process; + this.eventAggregator = eventAggregator; } public void OpenFile(string assemblyName, string qualifiedClassName, int file, int line) @@ -60,7 +62,7 @@ public void RateAndReview() public void DocumentFocused() { - FocusCallback(); + eventAggregator.SendMessage(new ReportFocusedMessage()); } public void ClearFCCWindowLogs() @@ -73,13 +75,5 @@ public void ShowFCCOutputPane() ShowFCCOutputPaneEvent?.Invoke(this, EventArgs.Empty); } - public object InvokeScript(string scriptName, params object[] args) - { - if(ScriptInvoker != null) - { - return ScriptInvoker.InvokeScript(scriptName, args); - } - return null; - } } } \ No newline at end of file diff --git a/SharedProject/SharedProject.projitems b/SharedProject/SharedProject.projitems index 1de6403a..3c7c9711 100644 --- a/SharedProject/SharedProject.projitems +++ b/SharedProject/SharedProject.projitems @@ -100,6 +100,7 @@ + @@ -159,7 +160,13 @@ + + + + + + @@ -168,6 +175,7 @@ +