From d3d3649a68cb1fa589fdd987a6690dbd5d671f0d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sun, 17 Sep 2017 15:35:20 -0700 Subject: Initial code commit --- .../AssemblyDefaultHeatExtensionAttribute.cs | 33 +++ .../Extensibility/HarvesterExtension.cs | 26 ++ src/WixToolset.Core/Extensibility/HeatExtension.cs | 204 ++++++++++++++ .../Extensibility/IHarvesterCore.cs | 62 +++++ src/WixToolset.Core/Extensibility/IHeatCore.cs | 36 +++ .../Extensibility/MutatorExtension.cs | 198 ++++++++++++++ .../Extensibility/ValidatorExtension.cs | 299 +++++++++++++++++++++ 7 files changed, 858 insertions(+) create mode 100644 src/WixToolset.Core/Extensibility/AssemblyDefaultHeatExtensionAttribute.cs create mode 100644 src/WixToolset.Core/Extensibility/HarvesterExtension.cs create mode 100644 src/WixToolset.Core/Extensibility/HeatExtension.cs create mode 100644 src/WixToolset.Core/Extensibility/IHarvesterCore.cs create mode 100644 src/WixToolset.Core/Extensibility/IHeatCore.cs create mode 100644 src/WixToolset.Core/Extensibility/MutatorExtension.cs create mode 100644 src/WixToolset.Core/Extensibility/ValidatorExtension.cs (limited to 'src/WixToolset.Core/Extensibility') diff --git a/src/WixToolset.Core/Extensibility/AssemblyDefaultHeatExtensionAttribute.cs b/src/WixToolset.Core/Extensibility/AssemblyDefaultHeatExtensionAttribute.cs new file mode 100644 index 00000000..bb53e30c --- /dev/null +++ b/src/WixToolset.Core/Extensibility/AssemblyDefaultHeatExtensionAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Tools +{ + using System; + + /// + /// Represents a custom attribute for declaring the type to use + /// as the default heat extension in an assembly. + /// + public class AssemblyDefaultHeatExtensionAttribute : Attribute + { + private readonly Type extensionType; + + /// + /// Instantiate a new AssemblyDefaultHeatExtensionAttribute. + /// + /// The type of the default heat extension in an assembly. + public AssemblyDefaultHeatExtensionAttribute(Type extensionType) + { + this.extensionType = extensionType; + } + + /// + /// Gets the type of the default heat extension in an assembly. + /// + /// The type of the default heat extension in an assembly. + public Type ExtensionType + { + get { return this.extensionType; } + } + } +} diff --git a/src/WixToolset.Core/Extensibility/HarvesterExtension.cs b/src/WixToolset.Core/Extensibility/HarvesterExtension.cs new file mode 100644 index 00000000..d8d0ab34 --- /dev/null +++ b/src/WixToolset.Core/Extensibility/HarvesterExtension.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset +{ + using Wix = WixToolset.Data.Serialize; + + /// + /// The base harvester extension. Any of these methods can be overridden to change + /// the behavior of the harvester. + /// + public abstract class HarvesterExtension + { + /// + /// Gets or sets the harvester core for the extension. + /// + /// The harvester core for the extension. + public IHarvesterCore Core { get; set; } + + /// + /// Harvest a WiX document. + /// + /// The argument for harvesting. + /// The harvested Fragments. + public abstract Wix.Fragment[] Harvest(string argument); + } +} diff --git a/src/WixToolset.Core/Extensibility/HeatExtension.cs b/src/WixToolset.Core/Extensibility/HeatExtension.cs new file mode 100644 index 00000000..5e292220 --- /dev/null +++ b/src/WixToolset.Core/Extensibility/HeatExtension.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Extensibility +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Reflection; + using WixToolset; + using WixToolset.Data; + using WixToolset.Extensibilty; + using WixToolset.Tools; + using Wix = WixToolset.Data.Serialize; + + /// + /// A command line option. + /// + public struct HeatCommandLineOption + { + public string Option; + + public string Description; + + /// + /// Instantiates a new CommandLineOption. + /// + /// The option name. + /// The description of the option. + public HeatCommandLineOption(string option, string description) + { + this.Option = option; + this.Description = description; + } + } + + /// + /// An extension for the WiX Toolset Harvester application. + /// + public abstract class HeatExtension + { + /// + /// Gets or sets the heat core for the extension. + /// + /// The heat core for the extension. + public IHeatCore Core { get; set; } + + /// + /// Gets the supported command line types for this extension. + /// + /// The supported command line types for this extension. + public virtual HeatCommandLineOption[] CommandLineTypes + { + get { return null; } + } + + /// + /// Loads a HeatExtension from a type description string. + /// + /// The extension type description string. + /// The loaded HeatExtension. + /// + /// can be in several different forms: + /// + /// AssemblyQualifiedName (TopNamespace.SubNameSpace.ContainingClass+NestedClass, MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089) + /// AssemblyName (MyAssembly, Version=1.3.0.0, Culture=neutral, PublicKeyToken=b17a5c561934e089) + /// Absolute path to an assembly (C:\MyExtensions\ExtensionAssembly.dll) + /// Filename of an assembly in the application directory (ExtensionAssembly.dll) + /// Relative path to an assembly (..\..\MyExtensions\ExtensionAssembly.dll) + /// + /// To specify a particular class to use, prefix the fully qualified class name to the assembly and separate them with a comma. + /// For example: "TopNamespace.SubNameSpace.ContainingClass+NestedClass, C:\MyExtensions\ExtensionAssembly.dll" + /// + public static HeatExtension Load(string extension) + { + Type extensionType = null; + int commaIndex = extension.IndexOf(','); + string className = String.Empty; + string assemblyName = extension; + + if (0 <= commaIndex) + { + className = extension.Substring(0, commaIndex); + assemblyName = (extension.Length <= commaIndex + 1 ? String.Empty : extension.Substring(commaIndex + 1)); + } + + className = className.Trim(); + assemblyName = assemblyName.Trim(); + + if (null == extensionType && 0 < assemblyName.Length) + { + + Assembly extensionAssembly; + + // case 3: Absolute path to an assembly + if (Path.IsPathRooted(assemblyName)) + { + extensionAssembly = ExtensionLoadFrom(assemblyName); + } + else + { + try + { + // case 2: AssemblyName + extensionAssembly = Assembly.Load(assemblyName); + } + catch (IOException e) + { + if (e is FileLoadException || e is FileNotFoundException) + { + try + { + // case 4: Filename of an assembly in the application directory + extensionAssembly = Assembly.Load(Path.GetFileNameWithoutExtension(assemblyName)); + } + catch (IOException innerE) + { + if (innerE is FileLoadException || innerE is FileNotFoundException) + { + // case 5: Relative path to an assembly + + // we want to use Assembly.Load when we can because it has some benefits over Assembly.LoadFrom + // (see the documentation for Assembly.LoadFrom). However, it may fail when the path is a relative + // path, so we should try Assembly.LoadFrom one last time. We could have detected a directory + // separator character and used Assembly.LoadFrom directly, but dealing with path canonicalization + // issues is something we don't want to deal with if we don't have to. + extensionAssembly = ExtensionLoadFrom(assemblyName); + } + else + { + throw new WixException(WixErrors.InvalidExtension(assemblyName, innerE.Message)); + } + } + } + else + { + throw new WixException(WixErrors.InvalidExtension(assemblyName, e.Message)); + } + } + } + + if (0 < className.Length) + { + try + { + // case 1: AssemblyQualifiedName + extensionType = extensionAssembly.GetType(className, true /* throwOnError */, true /* ignoreCase */); + } + catch (Exception e) + { + throw new WixException(WixErrors.InvalidExtensionType(assemblyName, className, e.GetType().ToString(), e.Message)); + } + } + else + { + // if no class name was specified, then let's hope the assembly defined a default WixExtension + AssemblyDefaultHeatExtensionAttribute extensionAttribute = (AssemblyDefaultHeatExtensionAttribute)Attribute.GetCustomAttribute(extensionAssembly, typeof(AssemblyDefaultHeatExtensionAttribute)); + + if (null != extensionAttribute) + { + extensionType = extensionAttribute.ExtensionType; + } + else + { + throw new WixException(WixErrors.InvalidExtensionType(assemblyName, typeof(AssemblyDefaultHeatExtensionAttribute).ToString())); + } + } + } + + if (extensionType.IsSubclassOf(typeof(HeatExtension))) + { + return Activator.CreateInstance(extensionType) as HeatExtension; + } + else + { + throw new WixException(WixErrors.InvalidExtensionType(extension, extensionType.ToString(), typeof(HeatExtension).ToString())); + } + } + + /// + /// Parse the command line options for this extension. + /// + /// The active harvester type. + /// The option arguments. + public virtual void ParseOptions(string type, string[] args) + { + } + + private static Assembly ExtensionLoadFrom(string assemblyName) + { + Assembly extensionAssembly = null; + + try + { + extensionAssembly = Assembly.LoadFrom(assemblyName); + } + catch (Exception e) + { + throw new WixException(WixErrors.InvalidExtension(assemblyName, e.Message)); + } + + return extensionAssembly; + } + } +} diff --git a/src/WixToolset.Core/Extensibility/IHarvesterCore.cs b/src/WixToolset.Core/Extensibility/IHarvesterCore.cs new file mode 100644 index 00000000..9a6fd10c --- /dev/null +++ b/src/WixToolset.Core/Extensibility/IHarvesterCore.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using WixToolset.Data; + using Wix = WixToolset.Data.Serialize; + + /// + /// The WiX Toolset harvester core. + /// + public interface IHarvesterCore + { + /// + /// Gets whether the harvester core encountered an error while processing. + /// + /// Flag if core encountered an error during processing. + bool EncounteredError { get; } + + /// + /// Gets or sets the value of the extension argument passed to heat. + /// + /// The extension argument. + string ExtensionArgument { get; set; } + + /// + /// Gets or sets the value of the root directory that is being harvested. + /// + /// The root directory being harvested. + string RootDirectory { get; set; } + + /// + /// Create an identifier based on passed file name + /// + /// File name to generate identifer from + /// + string CreateIdentifierFromFilename(string filename); + + /// + /// Generate an identifier by hashing data from the row. + /// + /// Three letter or less prefix for generated row identifier. + /// Information to hash. + /// The generated identifier. + string GenerateIdentifier(string prefix, params string[] args); + + /// + /// Sends a message to the message delegate if there is one. + /// + /// Message event arguments. + void OnMessage(MessageEventArgs mea); + + /// + /// Resolves a file's path if the Wix.File.Source value starts with "SourceDir\". + /// + /// The Wix.File.Source value with "SourceDir\". + /// The full path of the file. + string ResolveFilePath(string fileSource); + } +} diff --git a/src/WixToolset.Core/Extensibility/IHeatCore.cs b/src/WixToolset.Core/Extensibility/IHeatCore.cs new file mode 100644 index 00000000..bc853b24 --- /dev/null +++ b/src/WixToolset.Core/Extensibility/IHeatCore.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Extensibilty +{ + using WixToolset.Data; + + /// + /// The WiX Toolset Harvester application core. + /// + public interface IHeatCore + { + /// + /// Gets whether the mutator core encountered an error while processing. + /// + /// Flag if core encountered an error during processing. + bool EncounteredError { get; } + + /// + /// Gets the harvester. + /// + /// The harvester. + Harvester Harvester { get; } + + /// + /// Gets the mutator. + /// + /// The mutator. + Mutator Mutator { get; } + + /// + /// Sends a message to the message delegate if there is one. + /// + /// Message event arguments. + void OnMessage(MessageEventArgs mea); + } +} diff --git a/src/WixToolset.Core/Extensibility/MutatorExtension.cs b/src/WixToolset.Core/Extensibility/MutatorExtension.cs new file mode 100644 index 00000000..9de64180 --- /dev/null +++ b/src/WixToolset.Core/Extensibility/MutatorExtension.cs @@ -0,0 +1,198 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Extensibility +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Text; + + using Wix = WixToolset.Data.Serialize; + + /// + /// The base mutator extension. Any of these methods can be overridden to change + /// the behavior of the mutator. + /// + public abstract class MutatorExtension + { + /// + /// Gets or sets the mutator core for the extension. + /// + /// The mutator core for the extension. + public IHarvesterCore Core { get; set; } + + /// + /// Gets the sequence of the extension. + /// + /// The sequence of the extension. + public abstract int Sequence + { + get; + } + + /// + /// Mutate a WiX document. + /// + /// The Wix document element. + public virtual void Mutate(Wix.Wix wix) + { + } + + /// + /// Mutate a WiX document as a string. + /// + /// The Wix document element as a string. + /// The mutated Wix document as a string. + public virtual string Mutate(string wixString) + { + return wixString; + } + + /// + /// Generate unique MSI identifiers. + /// + protected class IdentifierGenerator + { + public const int MaxProductIdentifierLength = 72; + public const int MaxModuleIdentifierLength = 35; + + private string baseName; + private int maxLength; + private Dictionary existingIdentifiers; + private Dictionary possibleIdentifiers; + + /// + /// Instantiate a new IdentifierGenerator. + /// + /// The base resource name to use if a resource name contains no usable characters. + public IdentifierGenerator(string baseName) + { + this.baseName = baseName; + this.maxLength = IdentifierGenerator.MaxProductIdentifierLength; + this.existingIdentifiers = new Dictionary(StringComparer.OrdinalIgnoreCase); + this.possibleIdentifiers = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Gets or sets the maximum length for generated identifiers. + /// + /// Maximum length for generated identifiers. (Default is 72.) + public int MaxIdentifierLength + { + get { return this.maxLength; } + set { this.maxLength = value; } + } + + /// + /// Index an existing identifier for collision detection. + /// + /// The identifier. + public void IndexExistingIdentifier(string identifier) + { + if (null == identifier) + { + throw new ArgumentNullException("identifier"); + } + + this.existingIdentifiers[identifier] = null; + } + + /// + /// Index a resource name for collision detection. + /// + /// The resource name. + public void IndexName(string name) + { + if (null == name) + { + throw new ArgumentNullException("name"); + } + + string identifier = this.CreateIdentifier(name, 0); + + if (this.possibleIdentifiers.ContainsKey(identifier)) + { + this.possibleIdentifiers[identifier] = String.Empty; + } + else + { + this.possibleIdentifiers.Add(identifier, null); + } + } + + /// + /// Get the identifier for the given resource name. + /// + /// The resource name. + /// A legal MSI identifier. + [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "System.InvalidOperationException.#ctor(System.String)")] + public string GetIdentifier(string name) + { + if (null == name) + { + throw new ArgumentNullException("name"); + } + + for (int i = 0; i <= Int32.MaxValue; i++) + { + string identifier = this.CreateIdentifier(name, i); + + if (this.existingIdentifiers.ContainsKey(identifier) || // already used + (0 == i && 0 != this.possibleIdentifiers.Count && null != this.possibleIdentifiers[identifier]) || // needs an index because its duplicated + (0 != i && this.possibleIdentifiers.ContainsKey(identifier))) // collides with another possible identifier + { + continue; + } + else // use this identifier + { + this.existingIdentifiers.Add(identifier, null); + + return identifier; + } + } + + throw new InvalidOperationException(WixStrings.EXP_CouldnotFileUniqueIDForResourceName); + } + + /// + /// Create a legal MSI identifier from a resource name and an index. + /// + /// The name of the resource for which an identifier should be created. + /// An index to append to the end of the identifier to make it unique. + /// A legal MSI identifier. + public string CreateIdentifier(string name, int index) + { + if (null == name) + { + throw new ArgumentNullException("name"); + } + + StringBuilder identifier = new StringBuilder(); + + // Convert the name to a standard MSI identifier + identifier.Append(Common.GetIdentifierFromName(name)); + + // no legal identifier characters were found, use the base id instead + if (0 == identifier.Length) + { + identifier.Append(this.baseName); + } + + // truncate the identifier if it's too long (reserve 3 characters for up to 99 collisions) + int adjustedMaxLength = this.MaxIdentifierLength - (index != 0 ? 3 : 0); + if (adjustedMaxLength < identifier.Length) + { + identifier.Length = adjustedMaxLength; + } + + // if the index is not zero, then append it to the identifier name + if (0 != index) + { + identifier.AppendFormat("_{0}", index); + } + + return identifier.ToString(); + } + } + } +} diff --git a/src/WixToolset.Core/Extensibility/ValidatorExtension.cs b/src/WixToolset.Core/Extensibility/ValidatorExtension.cs new file mode 100644 index 00000000..44ec3106 --- /dev/null +++ b/src/WixToolset.Core/Extensibility/ValidatorExtension.cs @@ -0,0 +1,299 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Extensibility +{ + using System; + using System.Collections; + using WixToolset.Data; + + /// + /// Base class for creating a validator extension. This default implementation + /// will fire and event with the ICE name and description. + /// + public class ValidatorExtension : IMessageHandler + { + private string databaseFile; + private Hashtable indexedSourceLineNumbers; + private Output output; + private SourceLineNumber sourceLineNumbers; + + /// + /// Instantiate a new . + /// + public ValidatorExtension() + { + } + + /// + /// Gets or sets the path to the database to validate. + /// + /// The path to the database to validate. + public string DatabaseFile + { + get { return this.databaseFile; } + set { this.databaseFile = value; } + } + + /// + /// Gets or sets the for finding source line information. + /// + /// The for finding source line information. + public Output Output + { + get { return this.output; } + set { this.output = value; } + } + + /// + /// Called at the beginning of the validation of a database file. + /// + /// + /// The will set + /// before calling InitializeValidator. + /// Notes to Inheritors: When overriding + /// InitializeValidator in a derived class, be sure to call + /// the base class's InitializeValidator to thoroughly + /// initialize the extension. + /// + public virtual void InitializeValidator() + { + if (this.databaseFile != null) + { + this.sourceLineNumbers = new SourceLineNumber(databaseFile); + } + } + + /// + /// Called at the end of the validation of a database file. + /// + /// + /// The default implementation will nullify source lines. + /// Notes to Inheritors: When overriding + /// FinalizeValidator in a derived class, be sure to call + /// the base class's FinalizeValidator to thoroughly + /// finalize the extension. + /// + public virtual void FinalizeValidator() + { + this.sourceLineNumbers = null; + } + + /// + /// Logs a message from the . + /// + /// A of tab-delmited tokens + /// in the validation message. + public virtual void Log(string message) + { + this.Log(message, null); + } + + /// + /// Logs a message from the . + /// + /// A of tab-delmited tokens + /// in the validation message. + /// The name of the action to which the message + /// belongs. + /// The message cannot be null. + /// + /// The message does not contain four (4) + /// or more tab-delimited tokens. + /// + /// a tab-delimited set of tokens, + /// formatted according to Windows Installer guidelines for ICE + /// message. The following table lists what each token by index + /// should mean. + /// a name that represents the ICE + /// action that was executed (e.g. 'ICE08'). + /// + /// + /// Index + /// Description + /// + /// + /// 0 + /// Name of the ICE. + /// + /// + /// 1 + /// Message type. See the following list. + /// + /// + /// 2 + /// Detailed description. + /// + /// + /// 3 + /// Help URL or location. + /// + /// + /// 4 + /// Table name. + /// + /// + /// 5 + /// Column name. + /// + /// + /// 6 + /// This and remaining fields are primary keys + /// to identify a row. + /// + /// + /// The message types are one of the following value. + /// + /// + /// Value + /// Message Type + /// + /// + /// 0 + /// Failure message reporting the failure of the + /// ICE custom action. + /// + /// + /// 1 + /// Error message reporting database authoring that + /// case incorrect behavior. + /// + /// + /// 2 + /// Warning message reporting database authoring that + /// causes incorrect behavior in certain cases. Warnings can also + /// report unexpected side-effects of database authoring. + /// + /// + /// + /// 3 + /// Informational message. + /// + /// + /// + public virtual void Log(string message, string action) + { + if (message == null) + { + throw new ArgumentNullException("message"); + } + + string[] messageParts = message.Split('\t'); + if (3 > messageParts.Length) + { + if (null == action) + { + throw new WixException(WixErrors.UnexpectedExternalUIMessage(message)); + } + else + { + throw new WixException(WixErrors.UnexpectedExternalUIMessage(message, action)); + } + } + + SourceLineNumber messageSourceLineNumbers = null; + if (6 < messageParts.Length) + { + string[] primaryKeys = new string[messageParts.Length - 6]; + + Array.Copy(messageParts, 6, primaryKeys, 0, primaryKeys.Length); + + messageSourceLineNumbers = this.GetSourceLineNumbers(messageParts[4], primaryKeys); + } + else // use the file name as the source line information + { + messageSourceLineNumbers = this.sourceLineNumbers; + } + + switch (messageParts[1]) + { + case "0": + case "1": + this.OnMessage(WixErrors.ValidationError(messageSourceLineNumbers, messageParts[0], messageParts[2])); + break; + case "2": + this.OnMessage(WixWarnings.ValidationWarning(messageSourceLineNumbers, messageParts[0], messageParts[2])); + break; + case "3": + this.OnMessage(WixVerboses.ValidationInfo(messageParts[0], messageParts[2])); + break; + default: + throw new WixException(WixErrors.InvalidValidatorMessageType(messageParts[1])); + } + } + + /// + /// Gets the source line information (if available) for a row by its table name and primary key. + /// + /// The table name of the row. + /// The primary keys of the row. + /// The source line number information if found; null otherwise. + protected SourceLineNumber GetSourceLineNumbers(string tableName, string[] primaryKeys) + { + // source line information only exists if an output file was supplied + if (null != this.output) + { + // index the source line information if it hasn't been indexed already + if (null == this.indexedSourceLineNumbers) + { + this.indexedSourceLineNumbers = new Hashtable(); + + // index each real table + foreach (Table table in this.output.Tables) + { + // skip unreal tables + if (table.Definition.Unreal) + { + continue; + } + + // index each row + foreach (Row row in table.Rows) + { + // skip rows that don't contain source line information + if (null == row.SourceLineNumbers) + { + continue; + } + + // index the row using its table name and primary key + string primaryKey = row.GetPrimaryKey(';'); + if (null != primaryKey) + { + string key = String.Concat(table.Name, ":", primaryKey); + + if (this.indexedSourceLineNumbers.ContainsKey(key)) + { + this.OnMessage(WixWarnings.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, table.Name)); + } + else + { + this.indexedSourceLineNumbers.Add(key, row.SourceLineNumbers); + } + } + } + } + } + + return (SourceLineNumber)this.indexedSourceLineNumbers[String.Concat(tableName, ":", String.Join(";", primaryKeys))]; + } + + // use the file name as the source line information + return this.sourceLineNumbers; + } + + /// + /// Sends a message to the delegate if there is one. + /// + /// Message event arguments. + /// + /// Notes to Inheritors: When overriding OnMessage + /// in a derived class, be sure to call the base class's + /// OnMessage method so that registered delegates recieve + /// the event. + /// + public virtual void OnMessage(MessageEventArgs e) + { + Messaging.Instance.OnMessage(e); + } + } +} -- cgit v1.2.3-55-g6feb