Plugins Developer manual
Introduction
inPoint as a large framework consists of multiple layers (in a nutshell):
- A Server Module Layer running a WCF Service under a host (currently IIS)
- Multiple Clients running in Windows: Office/Windows Integration tools, Stand Alone Client, etc.
The Clients are usually made of:
- Front-End Core -- the main "glue" between all the components:
- In Standalone using the WPF Framework (inPoint.Forms.dll)
- In Office using a VSTO Wrapper (inPoint.Outlook.dll)
- In Explorer using the LogicNP NamespaceExtensions library (inPoint.Shell.dll)
- Middleware Service Layer -- aka HierarchyProvider -- to contact the standard Server Layer via WCF (inPoint.API.dll)
- Extension Plugins -- features implemented as extension points, either as Core Plugins or for prototyping (sort of beta-testing)
Plugins are a way to:
- Implement new functionality into inPoint, whether as a customer requested feature (e.g. Project specific) or as a tool for Prototyping a future Core feature
- Replace / Extend existing functionality implemented in inPoint Core (or any other Plugin) (e.g. Project specific)
MEF
inPoint uses MEF - The Managed Extensibility Framework - available as a built-in feature in .NET since v4.0, to allow us to create zero-footprint, no configuration, discoverable components that can either be created and used by the Core team itself or by anyone that wants to extends inPoint functionality.
A lot of the Core features are also implemented using MEF (e.g. all CommandActions, ArchiveDialog suggestions, etc…), included directly in the main library (inPoint.Forms.dll).
A simple explanation of how inPoint uses MEF:
- there are some predefined Interfaces which specify what inPoint is expecting the "part" (Plugin) to do, whether via input parameters or by output methods and with metadata (e.g. Name, Description)
- those "parts" can either be assembled in one library (dll) or multiple ones
- during inPoint start-up an AggregateCatalog is initialized which will compose those "parts" making them available to the Core library, using the
inPoint.API.Plugins.PluginManager
- depending on Configuration the required "parts" will be used across the Application (most are loaded only when required)
Client Plugins
Most of the provided Plugins are Client based, meaning they will run only in an active inPoint Client with a User in front; as such they are obviously mainly limited to GUI Plugins and less for background data management (although possible, they would not be activated in case of 3rd party access directly to the Server WCF Layer).
Infragistic Components
Since the HS.inPoint.Plugin NuGet has a reference to Infragistic components, only H&S Employees are allowed to compile it (Infrgistics License). Nugets and the VSIX extension should therefore also only be used by H&S Employees.
Server Plugins
Currently only two "kinds" of Server Plugins are included, the inPoint.Jobs (scheduled Tasks) and PamArchive Plugins (activated during archiving / retrieval of documents).
General Guidelines
All Interface definitions are included in the inPoint.API.Plugins
namespace.
All Plugin Metadata include at the minimum required a Name (unique identifier) and a Description (human readable). In the future, this will include also optional Versioning details, and some specific Plugins require additional metadata (e.g. PropertyTab define to which Items they apply, etc…).
Deployment
All Client Plugins reside on the Server, see Plugins File Structure
They are automatically downloaded during inPoint Startup, and as such allow for "easy" deployment across all Clients in the Enterprise.
NOTE: There is no "push" notification for Plugin changes, the Clients must manually restart.
Versioning
Currently there is no Versioning, but such a feature is planned for the future (at least as an optional feature).
Error Handling
Error Handling MUST be correctly handled by each Plugin, the Core library will simply log the error and show a MessageBox to the user for not handled Exceptions.
Logging
inPoint uses log4net across all it's components, but it is the Plugin's implementation that needs to take care of referencing/initializing the Logger.
Settings and Configuration
Currently only the Classification Plugins have built-in Settings management (PluginInfo), saved as part of the ElementFormat directly in the Database.
AttributeFlow StepActions also have some based on the same concept.
In the future we will extend this to all inPoint plugins (primarily for CommandActions), using a consolidated framework for saving/loading those Settings (based on the CascadingSettings feature).
inPoint Object Model
Some Core objects are passed as input parameters and sometimes expected as return values, so you need to know what they are all about. Here is a very high-level definition (all defined in inPoint.API or inPoint.Common):
HierarchyProvider
The Service layer to contact the Server. In 99% of the cases you do not need to create a proxy to PamWCF or UnifyWCF, the methods would be already implemented here, as Async methods always returning a TaskResult wrapper that will let you know whether the .OperationResult was successful (.Result will have any outputs) or failed (.Exception will know why). Provides methods for nearly every functionality from the client itself. This will be the most used object in your plugin since, it enables you to interact with the system (get documents and folders, download items, create documents etc.)
UIStateService
This is what the Core uses to identify the current UI state (as the name implies ;) Basically which Folder is currently selected, which Documents, etc…
ElementFormat SelectedElementFormat { get; set; }
ElementFormat SelectedDocumentFormat { get; }
HierarchyElement SelectedHierarchyElement { get; set; }
List<HierarchyDocument> SelectedDocuments { get; }
HierarchySiteType SelectedHierarchySiteType { get; set; }
ItemURI SelectedHierarchyElementItemURI { get; set; }
string SelectedHierarchyElementStringItemUri { get; }
List<string> SelectedDocumentsItemUris { get; }
PluginManager PluginManagerInstance { get; }
LazyHierarchyElement[] SelectedHierarchyElements { get; set; }
EventAggregator
inPoint uses Prism’s generic EventAggregator to pass messages across all it's modules, whether those are info events (e.g. SelectedHierarchyElementChangedEvent) or to request it to do something (e.g. OpenItemURIRequestedEvent or ArchiveDocumentRequestedEvent) – the latter allows for easy no hard-linking of Core inPoint features.
Can be used to trigger events in the client (e.g. a refresh of the tree).
For example, this will refresh the folder with the ItemURI folder.ItemUri
and all children below:
eventAggregator.GetEvent<TreeItemUriRefresh>().Publish(new TreeItemUriRefreshPayload()
{
ItemUri = folder.ItemUri,
ItemUriToSelect = folder.ItemUri,
RefreshMode = TreeRefreshMode.UpdateItemWithChilds
}
);
In a classification plugin you can subscribe to document field changes:
var token = eventAggregator.GetEvent<DocumentClassificationFieldChanged>().Subscribe((e) =>
{
object newValue = e.Value;
string commonName = e.Settings.CommonName;
DocumentField field = e.Settings.Field
});
If you subscribe to an event, you also have to unsubscribe if you are finished:
eventAggregator.GetEvent<DocumentClassificationFieldChanged>().Unsubscribe(token);
ElementFormat
This class contains all Folder/Document/Plugins definitions, named and specific to a certain Site/Category combination
HierarchyItem
Base class extending into HierarchyElement and HierarchyDocument for representing Folders and Documents respectively.
Templates
NuGet inPoint Repository
HS Release: http://vietfs.hs.local:8080/tfs/DefaultCollection/_packaging/HSRelease/nuget/v3/index.json
HS External: http://vietfs.hs.local:8080/tfs/DefaultCollection/_packaging/HSExternal/nuget/v3/index.json
Note that these need to be manually configured in your Visual Studio as a NuGet source!
Visual Studio Templates
inPoint.Plugins.SDK
is a VSIX Install Package which will pre-install all required templates and configurations in your Visual Studio installation, including the NuGet packages already pre-installed.
Project Template
The ProjectTemplate includes (you might need to manually adjust those!):
- A PostBuild script to deploy the .DLL to the target inPoint Server Plugin folder
- A Startup command-line in order to automatically start inPoint (with F5)
Item Templates
Classification Plugins
Scope
Classification Plugins are manually configured by the user directly in the ElementFormat ("FieldChooser"), and follow this strategy:
- Currently only ONE instance per Plugin can be used (e.g. no multiple ExtraAttributes)
- Each can be enabled/disabled (without the need to remove them) directly in the FieldChooser
- They are single-instanced by MEF – each time recreated and reloaded (so no static caching between Folders and/or Documents!)
- Each can have it's own Configuration Window (IPluginConfigWindow)
- All the settings are stored directly in the ElementFormat as part of the PluginInfo Object – the loading/saving is managed automatically
- They are fired for ANY item (Folders and Documents), it's up to the Plugin to decide on which one to act
- They are fired for: right panel, Edit Attributes, New and Edit
- There are no borders, margins, styles, etc… ANY UI settings whatsoever – it's up to the Plugin to decide how to integrate into the main app
- They should NOT reference Pam.Unify.Forms!
Getting a Plugins configuration
This is just an example, there is also a Helpers.Form.GetPlugInfo().
TODO: move to a Common library!
private ExtraAttributes.Config GetEAConfig()
{
var pinfo = elementFormat.Plugins.FirstOrDefault(p => p.PluginName.Equals("ExtraAttributes", StringComparison.InvariantCultureIgnoreCase));
if (pinfo != null)
return new ExtraAttributes.Config(pinfo.PluginConfig);
return null;
}
String Tokens
Often you want the user to be able to configure templates, e.g. for mails or setting other attributes. e.g. ExtraAttributes.FolderTemplateName = "{ContractId} – {LastModifiedName}"
There is a helper class under Helpers.Stuff.GetTokens
that uses RegularExpressions to retrieve those, you can then loop through them and replace whatever you need.
TODO: move to a Common library!
IClassification Interface
public interface IClassification : INotifyPropertyChanged
{
/// <summary>
/// Called to initialize the plugin.
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="UIStateInstance">The UI state instance.</param>
/// <param name="eventAggregator">The event aggregator.</param>
void Initialize(IHierarchyProvider provider, IUIStateService UIStateInstance, IEventAggregator eventAggregator);
/// <summary>
/// Gets the Viewer control used in a Panel when viewing an existing item.
/// </summary>
/// <param name="elementFormat">The element format.</param>
/// <param name="hierarchyItem">The hierarchy item (HierarchyElement or HierarchyDocument).</param>
/// <returns>the WPF UserControl to show; NULL if nothing</returns>
object GetViewControl(ElementFormat elementFormat, HierarchyItem hierarchyItem);
/// <summary>
/// Gets the control used in the Folder/Document Properties Window.
/// </summary>
/// <param name="elementFormat">The element format.</param>
/// <param name="hierarchyItem">The hierarchy item (HierarchyElement or HierarchyDocument).</param>
/// <returns>the WPF UserControl to show; NULL if nothing</returns>
object GetEditAttributesControl(ElementFormat elementFormat, HierarchyItem hierarchyItem);
/// <summary>
/// Gets the control used in the Folder/Document Properties Window when creating a new item.
/// </summary>
/// <param name="parentHierarchyElement">the target folder that will contain this new hierarchy item.</param>
/// <param name="elementFormat">The element format.</param>
/// <param name="hierarchyItem">The hierarchy item (HierarchyElement or HierarchyDocument).</param>
/// <returns>the WPF UserControl to show; if hierarchyItem is actually a HierarchyDocument,
/// you can return FALSE to skip the standard classification.</returns>
object GetCreateNewControl(HierarchyElement parentHierarchyElement, ElementFormat elementFormat, HierarchyItem hierarchyItem);
/// <summary>
/// Called when user decides to save his changes to the Classification form.
/// </summary>
/// <remarks>IF new item, this will get called twice:
/// 1. first time before actually saving the hierarchyItem (if hierarchyItem is HierarchyDocument)
/// then hierarchyItem.ItemUri will have the full path to the file
/// 2. second time after the saving, with the hierarchyItem.ItemUri filled out
///
/// For Move and Copy this will also get called twice
/// 1. first time before actually saving the hierarchyItem
/// 2. second time after the saving
///
/// Only for Update, SaveClassification in only called once before saving the hierarchyItem
/// </remarks>
/// <param name="hierarchyItem">The hierarchy item (HierarchyElement or HierarchyDocument).</param>
/// <returns>null if no changes, or the updated hierarchyItem (for new items in the first round)</returns>
HierarchyItem SaveClassification(HierarchyItem hierarchyItem);
/// <summary>
/// Returns a Window used to configure the Plugin.
/// </summary>
/// <remarks>any configurations should be stored directly in pluginInfo.PluginConfig</remarks>
/// <param name="elementFormat">The current element format.</param>
/// <param name="pluginInfo">The plugin information.</param>
/// <returns>the WPF Window to show; NULL if nothing</returns>
object GetConfigWindow(ElementFormat elementFormat, ElementFormatPluginInfo pluginInfo);
}
NOTE: all methods should return NULL when no implementation is needed.
GetXXXControl
NOTE: All the GetXXXControl()
methods expect a UserControl returned (and not just a simple UIElement) – otherwise nothing will be shown – this is to make sure WPF Bindings are correctly resolved.
GetViewControl()
– used to retrieve the View to show in the Right Panel (next to the Tree). This View currently exists only for FoldersGetEditAttributesControl()
– used to retrieve the View to show in the EditAttributes Window (first tab)GetCreateNewControl()
– used to retrieve the View to show when creating a new Folder or Document
SaveClassification
This method gets called when the user clicked OK and inPoint is going to create/update the item to the Server.
For existing items, it gets called before sending it to the server (itemUri is already known).
For new items, it actually gets called twice:
- Once before actually saving – allowing you to change anything in the HierarchyItem before sending to the Server (if HierarchyItem is a HierarchyDocument then the ItemUri will include the full path to the file)
- Once after saving – allowing you to do any post-processing once the ItemUri is known (i.e. when a new item was created)
Also when copying or moving items it's called twice.
IClassificationEx Interface
When you need more control on saving a HieraryhItem you can additionally implement the IClassificationEx interface.
public interface IClassificationEx
{
/// <summary>
/// Called when user decides to save his changes to the Classification form.
/// </summary>
/// <remarks>
/// Will be called before the first call to <see cref="IClassification.SaveClassification(HierarchyItem)"/>
/// and before saving the hierarchyItem.
///
/// This method can change (or replace) the hierarchyItem. The changed item is then used for later calls or
/// see <see cref="IClassification.SaveClassification(HierarchyItem)"/> or calls of
/// <see cref="BeforeSaveClassification{T}(T, ItemOperation)"/> of other plugins.
/// <returns>null if no changes, or the updated hierarchyItem</returns>
T BeforeSaveClassification<T>(T hierarchyItem, ItemOperation operation) where T : HierarchyItem;
/// <summary>
/// Called when user decides to save his changes to the Classification form.
/// </summary>
/// <remarks>
/// Will be called after hierarchyItem was saved and maybe after second call to <see cref="IClassification.SaveClassification(HierarchyItem)"/>
///
/// This method should never change the provided hierarchyItem.
/// <returns>null if no changes, or the updated hierarchyItem (for new items in the first round)</returns>
void AfterSaveClassification<T>(T hierarchyItem, ItemOperation operation) where T : HierarchyItem;
}
/// <summary>
/// Current operation when
/// <see cref="IClassificationEx.BeforeSaveClassification{T}(T, ItemOperation)"/> or
/// <see cref="IClassificationEx.AfterSaveClassification{T}(T, ItemOperation)"/>
/// is called.
/// </summary>
public enum ItemOperation
{
Unknown = 0,
Create,
Update,
Move,
Copy,
}
BeforeSaveClassification() is called before the item is saved. The current operation is provides as operation-argument. A new item can be returned and will be used for theBeforeSaveClassification / SaveClassification calls of other plugins. In this state some properties (for example the ItmeURI might not be initialized)
AfterSaveClassification() is called after the item was saved. This method should not modify the item.
Usually when implementing the IClassificationEx interface the SaveClassification() method should be an empty implementation returning null.
HierarchyItem
Classification Plugins can be written both to serve Folders, Documents or both.
In order to identify for which item it is currently being called on, do a check on the input parameter HierarchyItem.
E.g. if you only want it to be available to Folders, add this at the start of each method:
if (hierarchyItem is HierarchyDocument)
return null;
If you are indeed serving HierarchyDocuments, note that the ItemUri property will contain:
- For existing Documents, the real ItemUri of the Document
- For new documents (e.g. when user is archiving a new document and GetCreateNewControl() is called or in SaveClassification) – the full path to the file being archived (e.g.
c:\temp\myfile.docx
)
IPluginConfigWindow
public interface IPluginConfigWindow
{
Dictionary<string,string> PluginConfig { get; set; }
}
NOTE: This will be refactored once Plugin's Configuration/Settings are centralized.
ExtraAttributes Validation Plugins
Validation plugins are only called for the following ExtraAttributes types:
MaskedText
MaskedNumber
Date
Currency
IExtraAttributesValidationRule
public interface IExtraAttributesValidationRule
{
ValidationResult Validate(ObservableDictionary<string,object> form, string ruleParam, object value);
}
Following properties are passed on every user's key type (OnPropertyChanged):
form
A dictionary of ALL the attributes existing in the ExtraAttributes formruleParam
The custom Validation parameters configured in the "Validation Parameter" for that specific ExtraAttribute's attribute (in the FieldChooser config)value
The current value typed by the user (before it's actually commited to the form)
You can return ValidationResult with:
TRUE
Validation successfulFALSE
Validation failed and the message to the show to the user
NOTES:
- The .Validate() will be called EVERY time a field in the form changes – not only the one you applied the ValidationRule to (otherwise it wouldn't know when other fields change) – so it's up to YOU to make sure it's quick / cached
- although not supported, it IS currently possible to change the actual form values (others or specifically this one), e.g. for changing all to uppercase
- currently defined only in inPoint.Plugins.dll, this will be refactored when implemented directly in Core and extended to validate also the "standard" inPoint Attributes.
ArchiveDialog Suggestion Plugins
IArchiveSuggestion
public interface IArchiveSuggestion
{
double ScoreWeight { get; }
void Initialize(IHierarchyProvider provider, IUIStateService UIStateInstance);
ArchiveSuggestionTarget[] GetTargets(Dictionary<string, object> sourceProperties);
}
CommandAction Plugins
ICommandAction
public interface ICommandAction : INotifyPropertyChanged
{
/// <summary>
/// Called when context menu gets registered.
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="UIStateInstance">The UI state instance.</param>
void Initialize(IHierarchyProvider provider, IUIStateService UIStateInstance, IEventAggregator EventAggregator);
/// <summary>
/// Gets the command verb - basically the unique identifier for this CommandAction
/// </summary>
/// <value>
/// The command verb.
/// </value>
string CommandVerb { get; }
/// <summary>
/// Gets or sets the command option as set in the PAM_CUSTOMACTIONS table (e.g. for re-using the
/// same CommandAction with multiple options)
/// </summary>
/// <value>
/// The command option.
/// </value>
string CommandOption { get; set; }
/// <summary>
/// Gets or sets the command parameter - when this CommandAction is called directly (and not
/// from a ContextMenu)
/// </summary>
/// <value>
/// The command parameter.
/// </value>
object CommandParam { get; set; }
/// <summary>
/// Action() delegate called by context menu (based on current UIStateService selection)
/// </summary>
void Execute();
/// <summary>
/// Enable/Disable Context menu item.
/// </summary>
/// <returns></returns>
bool CanExecute();
/// <summary>
/// Whether this item should be visible at all (based on current UIStateService selection)
/// </summary>
/// <returns></returns>
bool ShouldBeVisible();
List<CommandActionItem> GetItems();
}
CommandActionItem (submenus)
public class CommandActionItem
{
public string Header { get; set; }
public string ImageSource { get; set; }
public Action Execute { get; set; }
public Func<bool> CanExecute { get; set; }
public bool IsEnabled { get; set; }
}
ICommandCondition
public interface ICommandCondition
{
/// <summary>
/// Called when context menu gets registered.
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="UIStateInstance">The UI state instance.</param>
void Initialize(IHierarchyProvider provider, IUIStateService UIStateInstance, IEventAggregator EventAggregator);
/// <summary>
/// Returns whether this item should be Visible.
/// </summary>
/// <param name="conditionParam">The condition parameter.</param>
/// <returns></returns>
bool ShouldBeVisible(string conditionParam);
}
AttributeFlow StepAction Plugins (ICommandAction)
PropertyTab Plugins
IPropertyTab
public interface IPropertyTab
{
string Header { get; }
string Icon { get; }
bool IsDirty { get; set; }
bool IsValid { get; set; }
/// <summary>
/// Initializes the specified provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <param name="UIStateInstance">The UI state instance.</param>
/// <param name="EventAggregator">The event aggregator.</param>
void Initialize(IHierarchyProvider provider, IUIStateService UIStateInstance, IEventAggregator eventAggregator, PropertyTabHost currentHost);
/// <summary>
/// Enable/Disable tab item.
/// </summary>
/// <returns></returns>
bool IsEnabled(HierarchyItem hierarchyItem);
/// <summary>
/// Whether this Tab should be visible at all
/// </summary>
/// <returns></returns>
bool IsVisible(HierarchyItem hierarchyItem);
/// <summary>
/// Gets the View control to render in the Tab
/// </summary>
/// <param name="elementFormat">The element format.</param>
/// <param name="parentHierarchyElement">The parent's hierarchy element.</param>
/// <param name="hierarchyItem">The hierarchy item.</param>
/// <returns></returns>
object GetViewControl(ElementFormat elementFormat, HierarchyElement parentHierarchyElement, HierarchyItem hierarchyItem, object obj = null);
/// <summary>
/// Gets the custom commands identifiers
/// </summary>
/// <returns></returns>
List<IPropertyTabCommand> GetCustomCommands();
/// <summary>
/// Executes the command identified by ID (either Standard from Host or Custom)
/// </summary>
/// <remarks>custom IDs predefined by host: SAVE, CANCEL</remarks>
/// <param name="ID">The identifier.</param>
/// <param name="hierarchyItem">The hierarchy item.</param>
/// <returns></returns>
bool ExecuteCommand(string ID, HierarchyItem hierarchyItem);
}
FAQ
Running async methods synchronous
Many hierarchy provider methods (e.g. provider.FolderItem_GetWithLinksAsync
) are async, so that the will return immediately, even if the method execution isn't finished yet.
If you don't want this behaviour, you can execute every method synchronous
The NuGet package HS.inPoint.Common provides the static class Pam.Unify.Common.AsyncHelpers
.
Which has 2 methods that allow you to run async methods (which return some kind of Task
) synchronous.
//
// Summary:
// Execute's an async Task<T> method which has a void return value synchronously
//
// Parameters:
// task:
// Task<T> method to execute
public static void RunSync(Func<Task> task);
//
// Summary:
// Execute's an async Task<T> method which has a T return type synchronously
//
// Parameters:
// task:
// Task<T> method to execute
//
// Type parameters:
// T:
// Return Type
public static T RunSync<T>(Func<Task<T>> task);
So, if you need to run provider.FolderItem_GetWithLinksAsync
sync it should look like this:
var syncCall = AsyncHelpers.RunSync(() => provider.FolderItem_GetWithLinksAsync(itemUri));
if(syncCall.OperationResult == HierarchyProviderOperationResult.Success)
{
//syncCall.Result holds the retrieved TreeItem
var myItem = syncCall.Result;
}
else
{
//Do something in case the call failed
}
If the call has failed, the OperationResult
will be Failed
and you can retrieve the fault details from the returned object.