ServiceAPI
- all actors (web service, client, WGS) are using the same time-zone settings, and
- dates are always sent as UTC (not local) dates.
- uses the Origin with Uri 9000000006,
- does not use background processing (processing is completed within the HTTP post request),
- creates one Location and one Record,
- uses the Location created for the Record assignee,
- assumes that the file "test doc.docx" has already been uploaded (using the UploadFile service), and
- should be posted to http://MyServer/ServiceAPI/BulkLoader.
- some Record properties are very expensive so response times will be unnecessarily slow, and
- some properties will throw an error if the related feature is not enabled (e.g. NextTaskDue requires the Vital Records feature be enabled).
- use the 'ViewPane' property set instead, which returns a list of all properties that might be interesting to an end user, or
- construct a custom property set in hptrim.config which can contain all the properties that are of interest to your application.
- go to the ServiceAPI server
- edit the file \RazorPages\GlobalErrors.cshtml
- add the code 'this.Response.StatusCode = 404;' into the header (see example below)
- identify the Record for which you want a new version,
- create a Record object using the original Record Uri,
- set the NewType property,
- update any properties you want to be different on the new version (for example: set the Notes to empty, then
- post the Record object as per usual.
- each message id is surrounded with ^ and $ to ensure an exact match,
- the pipe seperates each message id, and
- the messages are returned in the order found in the RM localisation not in the order you request them.
- each file uploaded in a seperate request,
- checks file size before upload, and
- update RecordDateCreated with file last modified date.
- add Json to serviceFeatures
- add the following route (in routeDefaults)
- includes a file upload form, and
- sets the RecordDateCreated from the file object using Javascript.
- a communication must be related to a record, the Record property controls this.
- a communication must have a sender and recipient and these must be HPRM location objects, if you do not have a location for the recipient you must create one.
- the ChildDetails is a child list as described in this post.
- a first look at event processor add-ins,
- creating a ServiceAPI plugin
- there are many posts discussing WebDrawer customisation, here is one.
- it will degrade performance, and
- it writes potentially sensitive information (such as a user's session id) to the log file.
- create a Google Client Id
- add the appropriate settings to the WebDrawer config files, and
- copy the required DLLs.
- GoogleOAuth DLLs, and
- source code.
- not having to write a file a file unnecessarily, and
- avoiding the non-thread safe approach in the previous post.
- height from the window heigh less the top position of the iframe (plus a 5 pixel buffer), and
- width to the window width less the iframe margin (10 pixels) plus the menu width (if the left menu is on the left).
Powershell confidential client
Use the sample code to connect to the ServiceAPI from powershell using the confidential client authentication flow.
Partial file download
I was asked recently about downloading files from the ServiceAPI using the Content-Range header to support chunked downloads. I was sure that this was supported but it was not working. After some hours of digging I found a problem that caused normal document downloads to ont recognize Content-Range, oddly enough the service to return multiple documents as a zip was working.
Object Notifier
The Object Notifier is an event based system to notify you when objects in Content Manager have changed, it can be used in a ServiceAPI application to proactively update your UI to refleect changes in the data, this sample implements the Object Notifier in the WebDrawer Record search results.
Sample OpenAPI Definition File
The swagger definition file that is auto-generated by the ServiceAPI swagger UI is not particularly useful due to its large size and complexity, in fact it often causes swagger tools to crash. For this reason it is better to handcraft an OpenAPI definition file to respresent the portions of the ServiceAPI you need. The sample is in the samples repo, the following video is a run through of making a change to the file and generating test code.
Web Service Nuget Package
For those who write .Net applications there is a new Nuget package and sample code to simplify the development process. The package contains both .Net 45 and standard assemblies so is of use in most .Net scenarios (e.g. native Windows, .Net Core, Xamarin).
The Polling endpoint
Logic apps triggers were discussed in a previous post, these triggers rely on the polling endpoint in the ServiceAPI. The polling endpoint is simply a special purpose search designed to support the request/response pattern used by triggers. This pattern expects a response containing records created or updated since the last request.
Composing a search string
Search strings are required for searching in the web service and can be convenient in the .Net SDK, there are a few rules on how to compose these strings but if I need to compose a complex search to get baked into an application I like to use the Content Manager client to assist me.
Avoid setting DateLastUpdated when downloading a document
The default behaviour in Content Manager is to update the data LastUpdated when getting an electronic document from a Record. This can be problematic, particularly in cases where you want to get the document every time the Record changes. Thankfully this update can be override.
Logic Apps (and Flow) CM trigger and send to Microsoft Teams
In a previous post I described my initial experiments with Microsoft Logic Apps. For this post I have created a trigger on Content Manager so that the Logic App can react to modifications to Records. To make this work I needed to make a small modification to the ServiceAPI code, this could be applied to an existing CM installation via a ServiceAPI plugin.
Logic Apps (and Flow) Custom Connector
Connect to Content Manager from Microsoft Logic Apps (or Flow) using a custom connector. A new, very simple, OpenAPI definition is available to assist in creating the Logic Apps custom connector.
Workaround for incorrect date returned in Additional Fields
When posting a date to an Additional Field in the web service the date returned in the response is incorrect, even though the date set in CM is correct. The problem is caused by a bug in handling time zone offsets. The scenarios in this post assume that:
Query CM from Excel
One of the outputs from my recent experimentation with PowerBI and Power Query is this saved search connector. I do not spend much time in Excel myself but I know that it is popular out there in the real world. If you would like to see something more/different contact me on the forum or through the usual channels.
Return a JSON error response from file download
If you prefer to receive a JSON error response when downloading a file then append format=json in the query string.
Post a Record with HttpClient
If you are not using the full .Net framework, maybe you have a .Net standard library in a Xamarin application, then you will not have access to System.net.WebClient. In place of this use System.Net.Http.HttpClient. This new sample is a very simple console app demonstrating how to post a JSON request.
ServiceAPI Bulk Loader
The ServiceAPI exposes a bulk loading capabilities for which a C# sample is provided in the help. This blog post shows the actual JSON that is sent over the wire.
The first step is to create an Origin object in the client, once this is done a batch of Records can be posted to the bulk loader. The sample below:
{ "Origin": { "Uri":9000000006 }, "AutoCommitLocations": true, "SQLCheckConstraints": false, "SQLServerTableLock": true, "UseBulkLoaderRecordNumbering": true, "ProcessInBackground":true, "LocationsToImport":[{ "LocationSurname":"Jones", "LocationGivenNames":"Arthur", "LocationTypeOfLocation":"Person" }], "RecordsToImport": [ { "RecordTitle": "My title from BL 5.1 AAAAA", "RecordAssignee":{ "LocationSurname":"Jones", "LocationGivenNames":"Arthur" }, "RecordFilePath":"test doc.docx" } ] }
Background processing
If 'ProcessInBackground' is set to true then the ServiceAPI will hand the bulk loading request off to the TRIMServiceAPIBulkLoader service (installed as a windows service). In this case the bulk loading request will immediately return an OriginHistory object. The Uri from this object can be used to poll the OriginHistory object to find out when the bulk loading operation has completed. The Url to fetch the OriginHistory will look like this:
http://localhost/ServiceAPI/OriginHistory?q=uri:9000000019&properties=BulkLoaderIsRunning,OriginHistoryRecordsCreated
Demo
In the video below I demonstrate bulk loading one Record using background processing.
Recursive searching in WebDrawer and ServiceAPI
The CM search syntax allows for recursive searching, for example returning results from folders contained within folders. The operator that specifies recursion is the plus sign (+), the problem is that in a URL the plus sign is reserved to indicate white space.
To overcome this the ServiceAPI (and WebDrawer) use the tilde (~) instead of the plus sign, so instead of this:
container:[title:asia]+
Do this:
container:[title:asia]~
If you would rather use a character (or sequence of characters) other than the tilde you can customise this in the hptrim.config. The Web Client, for example, uses _$_ instead of tilde, which is set in the 'searching' element in its hprmServiceAPI.config.
<searching pageSize="30" searchRecursiveOption="_$_"/>
The 'All' property set in the ServiceAPI
Overview
In the ServiceAPI there is a concept of a propertySet, this allows for the retrieval of a pre-defined set of properties, for example:
/Record?q=all&propertySets=Grid&format=json
The problem with 'All'
One of the property sets is 'All' which means that the above URL could look like this:
/Record?pageSize=20&q=all&propertySets=All&format=json
Fetching all properties for many object types (such as Classification, SavedSearch, Activity etc) can be a good idea, being simpler than pre-determining which properties you need. Using 'All' for Records is problematic, two problems being:
The solution
The solution is not to use 'All'. The alternatives are:
Creating a child object (Record 'Alt in' Relationship)
The code
To add a new object to one of the child object collections on a TrimMainObject, for example adding a RecordRelationship to a Record, requires sending an array containing one or more objects to the appropriate collection. For Example, posting the code below to the Record endpoint will cause Record 9000000002 to be 'Alternately contained within' Record 9000008050.
{ "Uri":9000000002, "ChildRelationships":[ { "RecordRelationshipRelatedRecord":9000008050, "RecordRelationshipRelationType": "IsAltIn" }] }
Using a search query instead of a Uri in a URL
Recently I wanted to link to a Record using the external ID, not the Uri or the number. This was from a customised WeDrawer page so I could have done a search to find the Uri and then used that, but I didn't want that extra step. A little reflection (and poking around in the code) and I realised that I could use the external id in the URL after all.
If it was a search
A search by external id would look like this, problem was I want to open the actual record page, not a list of one record.
/WebDrawer/record?q=recExternal:abc123
As a URL
As long as a search returns only one result then you can substitute the query for the Uri in the URL, as long as you can compose the query without using a colon (colons are reserved characters in URLs). So below I use equals instead of colon to either open the details page or go directly to the document via the external id.
http://localhost/WebDrawer/record/recExternal=abc123 http://localhost/WebDrawer/record/recExternal=abc123/file/document
Note
As is my habit I use the internal name for the search clause (recExternal) not the name (external) to minimise the possibility of breakages due to either custom captions or use of different languages.
SAML signout
Overview
In a previous post I show how to use Component Space to add SAML support to the Web Client, here I add a sign-out button.
Steps
useADFS == true
Set useADFS to true in hprmServiceAPI.config, for example:
<setup databaseId="J1" searchAhead="false" advancedSearch="false" workpath="C:\HP Records Manager\ServiceAPIWorkpath\Uploads" useADFS="true"/>
Setup signing certificate
If you plan to sign your logout request then you will need a certificate, either encrypt the password or store the certificate in the Windows certificate store as described in this Component Space document.
Add keep-alive
To allow for notification when the user's session has expired add the keep alive loop. To do this edit _Initialisation.cshtml (or _Shared.cshtml in later versions) and comment out the RMStayALive function, then add this new version:
var RMStayALive2 = function () { var makeRequest = function () { $.getJSON(HP.HPTRIM.TrimClient.getServiceAPIUrl() + "/Location/me", function (data, status, xhr) { }).fail(function () { if (confirm("Your session has expired, do you wish to re-authenticate?")) { top.location = HPRMWebConfig.virtualDirectory; } }); } setInterval(makeRequest, (60 * 1000));
SAML support
The Content Manager web applications (Web Client, WebDrawer and ServiceAPI) do not have native support for SAML authentication, this plugin uses Component Space to add SAML support. In the video below I walk through building and installing the plugin and linking it up to a SAML application in Azure AD.
Java ServiceAPI and additional fields
Previous posts have described the Java client and file upload. This post details how to fetch the value of additional fields.
Field Names
As described elsewhere the name to use to fetch an additional field is its search clause name, as seen in the client.
ServiceAPI - Glitch in 9.1.1 and 9.2 upgrade
If you are upgrading the ServiceAPI or WebDrawer to 9.1.1 or 9.2 you may get an error like the one below when you open via a web browser. This is due to a glitch in the way we upgrade the config files.
Where to store config, web.config or not...
For those who customise WebDrawer on one machine and then push it to a production machine this may prove handy. I wanted to change the setup of IIS on my dev machine to not update the web.config when I change the authentication settings. Here is what I did...
1. Go to Feature delegation in IIS Manager
Upload a file and create a new rendition
I just added a new sample to the documentation that ships with the ServiceAPI. In the mean-time here it is, the two step process to first upload a file and then use it to create a new Rendition on a new Record.
You could also use Record.FilePath to use the uploaded file to create the electronic document for the Record itself.
FileInfo fi = new FileInfo("c:\\junk\\test.pdf");
Custom Authentication
A couple of years I wrote a post describing Google authentication in the ServiceAPI. This is an update of that. The details are in the samples repo and here is a video of me applying the sample from the repo to a WebDrawer and Web Client instance.
Other authentication providers
Remember the repo contains a link the ServiceStack documentation which provides assistance for using (and creating) other authentication providers.
ServiceAPI tracing and dates
Enabling tracing in the ServiceAPI can help diagnose problems, in this video I use it to have a look date type additional fields.
Java file upload
The ServiceStack client we use to connect to the ServiceAPI from Java builds a set of classes to simply creating and requesting objects but it has no facility to upload an electronic document. Given the diversity of HTTP libraries in the Java world this probably makes sense.
This sample demonstrates uploading a file using the Apache HttpClient.
Be careful with search grammar items in string searches
I was caught out the other day using hard-coded string searches in a ServiceAPI application. It can be convenient (or necessary) to store canned searches (e.g. 'extension:docx OR extension:doc') at times. The thing to remember is that search strings can be localised and your end user may not be using English.
First, the clauses
Typically the English versions of the search clauses should work irrespective of the user's language, so even if the end user has selected French ' extension' should still be OK. Due to my over cautious nature I still use the internal name (e.g. 'recExtension) which can be found in the list of search clauses in the ServiceAPI.
Then the grammar items
Search grammar items (such as 'or' and 'and') are not language neutral so 'recExtension:doc or recExtensiom:docx' will not produce any results if your end user has selected Dutch as their language, what you will need is 'recExtension:doc of recExtensiom:docx'.
SDK
Getting the caption for a search grammar item in the SDK is simple:
EnumItem item = new EnumItem(AllEnumerations.SearchGrammarItem, (int)SearchGrammarItem.And).Caption
ServiceAPI - .Net
It is nearly as simple to get the grammar items using the ServiceAPI .Net client.
TrimClient client = new TrimClient("http://localhost/ServiceAPI"); client.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
Using Postman to create a record
Previously I experiment with searching using Postman Today lets create a record and upload a file in the same request.
Some Code
Here is the HTTP I generated using Postman.
POST /ServiceAPI81/record HTTP/1.1 Host: desktop-39dgcn3 Authorization: Basic aXR1X3RhZG1pbjpUcmltQEhQMQ== Accept: application/json Cache-Control: no-cache Postman-Token: b9575896-9e99-ccf0-4803-452e4fba11d4 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
ServiceAPI impersonation
The configuration section of the ServiceAPI help documents a handy little property called 'trustedToImpersonate'. This allows your ServiceAPI to choose to trust one or more calling services.
Scenario
This can be useful in a server to server scenario where passing the actual user credentials to the ServiceAPI is not practical.
Configuration
To allow a particular account to impersonate others users set their name in the hptrim.config file as seen below. TrustedToImpersonate is a regular expression so you can list multiple accounts if you need to.
<hptrim serviceFeatures="Razor,Html,Json,Xml,PredefinedRoutes" trustedToImpersonate="trim\\davidc" ... </hptrim>
Usage
To impersonate someone using the .Net client librarys use the SetUserToImpersonate() method.
TrimClient client = new TrimClient("http://MyServer/ServiceAPI"); client.Credentials = new NetworkCredential("davidc", "my password", "trim"); client.SetUserToImpersonate("someone\\else");
To impersonate from a different context simply send an HTTP header named 'userToImpersonate' with the value being the user's name along with each request.
ServiceAPI searching, GET or POST
It had been in the back of my mind that using HTTP GET for searching in a project I was working on would lead to trouble and several hours of re-factoring later I am proved correct.
Background
The default way we do searching in the ServiceAPI is using HTTP GET, for example:
http://localhost/ServiceAPI/Record?pageSize=20&q=all&filter=extension:docx,xlsx&properties=RecordTitle&format=json
There are several advantages to use the GET verb but there is also a trap which is that IIS has limitations on the length of a URL. Usually this is not a problem and the ServiceAPI has design features to help keep URLs concise (such as custom property sets) however I encountered a scenario where I could not avoid the IIS error resulting from an overlong URL.
The scenario
The scenario that caught me was needing the flexibility for the end user to specify as many properties in the properties query parameter as they desired and I did not want to use the 'all' property set as I did not want the cost of fetching every record property. If the user chose too many properties their request would fail.
The solution
The solution is that the ServiceAPI allows you to POST the same request that you usually GET, you just have to post it to a different end point. There are a couple of examples of this using an form post and AJAX in the documentation, here is an example using the .Net client.
TrimClient client = new TrimClient("http://localhost/ServiceAPI"); client.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials;
Java ServiceAPI client
Prior to Content Manager 9.0 there was no supported Java client for the ServiceAPI, although I did publish an experimental project. As of 9.0 we are able to use the ServiceStack Java client. This video demonstrates that client.
The code
The sample project I created in the video is on our samples site. Beware, in the video I omit the call to 'setAlwaysSendBasicAuthHeaders', you should always call this when using basic authentication.
CORS with Basic or Windows Authentication
Update
Along with one of our customers I rethought the approach below to integrate everything into the module, see the sample for more details.
Intro
In the ServiceAPI help there is a sample on how to use the CORS feature. There are some limitations in this feature that make it unlikely to work for most ServiceAPI users.
Preflight
When using a browser, maybe via jQuery, the odds are that a preflight request will be sent. This is an HTTP OPTIONS request to confirm that the correct CORS headers are returned. This request is sent without any authentication. The problem when using IIS basic or integrated authentication is that this request will be rejected.
The solution
The solution is not to use the CORS feature at all but to fall back on traditional ASP.Net techniques, as detailed in this very helpful post .
Module
Firstly write a, ASP.Net Module to allow us to get into the pipeline before IIS rejects our request, compile this in Visual Studio and place the DLL in the ServiceAPI bin folder. All OPTIONS requests will now be returned with the status 200.
public class CORSModule : IHttpModule { public void Dispose() { }
Take care when 'finding' a Record by number
In a recent post I discussed the FindBy method. This can significantly reduce the load on a server by eliminating unnecessary requests. When searching for a record by record number, it is faster to find than to search.
Search Examples
This is faster:
/Record/rec_1?format=json
Than this:
/Record?q=number:rec_1&format=json
FindBy Examples
This is faster:
{ "RecordTitle": "My Title", "RecordContainer": {"FindBy":"REC_1"} }
Than this:
{ "RecordTitle": "My Title", "RecordContainer": {"FindBy":"number:REC_1"} }
Caveats
Firstly, if you have numeric record numbers they may clash with the record Uri so you will have to do a search and use the 'number' clause.
Omitting the 'number' clause from a FindBy causes the 'default' search to be used which also includes the title, this may result in false positives.
Summary
On systems under heavy load you may consider avoiding the number clause as long as you write your code to filter out any false positive search results that result from doing so.
ServiceAPI ADFS OAuth configuration and sample
For those interested I just added a new sample to the community site.
Sending service actions with PostFileWithRequest
The PostFileWithRequest operates differently to the normal Post request in that instead of sending a simple JSON object it sends a multi-part form request with the object (e.g. a Record) serialized not to JSON but as an HTML form. This means that some complex constructs, such as the Record.AccessControlList, do not translate well.
Service Actions, sent in ActionsToCall, nearly work except for the code that specifies the type of Service Action. The good news is that this can be tweaked on the client side by overriding the code to emit the type name. In the sample below the line JsConfig.TypeWriter... called after TrimClient is instantiated, should allow any Service Action to be de-serialized by the ServiceAPI.
TrimClient trimClient = new TrimClient("http://MyServer/ServiceAPI"); trimClient.AlwaysSendBasicAuthHeader = true; trimClient.UserName = "USERNAME"; trimClient.Password = "PASSWORD";
404 Status code not returned
There are instances when an error in the ServiceAPI will return an HTTP status 200, rather than one of the 400 status codes. Unfortunately there are one or two cases where an internal exception falls outside of the standard error pipeline, this is something to be addressed in a future release.
A workaround
Any exception not handled will fall back to using a global error handler, at the moment this handler is not setting the status code, meaning it defaults to 200. You can fix this yourself. To do this:
Optimise a ServiceAPI post with FindBy
The most obvious way to specify a RecordType, Container, Location or other object property is using its Uri, for example you might post this to create a new Record:
{ "RecordTitle": "My Title", "RecordRecordType": {"Uri": 1} }
FindBy
If you know the Uri then all is good, if not you will have to make a separate request to find the Uri. If you do not know the Uri but do have some other unique information about the object, such as its name, or relationship to you (e.g. supervisorOf:Me) then you may use the FindBy, for example:
{ "RecordTitle": "My Title", "RecordRecordType": {"FindBy":"name:Document"} }
This method may be used with a standard and multipart form post as well, simply post the string rather than the Uri, for example:
------WebKitFormBoundary8Pd5W3BlMsyEl0k8 Content-Disposition: form-data; name="RecordRecordType"
Attaching contacts to a Record using the ServiceAPI
A further update
Was not thinking as clearly as I could have been last time I posted. The best way to get the contacts is via the ChildLocations property as this will tell you the relationship type, for example:
Records request = new Records(); request.q = "REC_349"; request.Properties = new PropertyList("ChildLocations");
ServiceAPI plugin to get a TR5 reference
It had never occurred to me that one would want to get a TR5 reference to open a Record in the native client from the ServiceAPI, but it appears some people want this. I have added a sample in our new Samples site demonstrating a plugin you can build and add to your ServiceAPI instance.
WebDrawer (and Web Client) auto filter search
While all users may configure default filters and have ACLs restricting access to certain records it can be desirable to configure an instance of WebDrawer to only return a subset of Records. Maybe this is the planning portal and you only want it to return planning permission related Records?
Filter WebDrawer
The hptrim.config file contains a section called routeDefaults, there are two of these elements you will need to modify. The first has the model 'Records', add a filter attribute as seen below. This example filters out all .doc files.
<add name="Record" model="Records" template="WDRecordList" filter="not extension:doc" properties="RecordRecordType,RecordExtension,RecordTitle,RecordNumber,RecordIsElectronic" />
If you are using custom search forms you will also need to filter the FormSearch route.
<add name="FormSearch" model="FormSearch" filter="not extension:doc" properties="RecordRecordType,RecordExtension,RecordTitle,RecordNumber,RecordIsElectronic" />
Filter Web Client
The Web Client may be similarly filtered, edit hptrimServiceAPI.config and edit the Records route, as seen here:
<add name="Record" model="Records" filter="not extension:doc" sortBy="recRegisteredOn- unkUri" includePropertyDefs="true" />
Saved Searches
A slightly more elegant twist on the above approach is to not use the actual search string but to use a saved search. The example below filters using the Saved Search named 'NotDocuments'.
<add name="Record" model="Records" template="WDRecordList" filter="saved:NotDocuments" properties="RecordRecordType,RecordExtension,RecordTitle,RecordNumber,RecordIsElectronic" />
Be Careful
It is best to not copy the XML snippets directly to your config file as the other attributes may be different in your version. Just add the filter attribute.
Use 'local' when running on the Workgroup Server
One tip that is not as well known as it should be is the use of the reserved work 'local' to refer to a local Workgroup Server. Any time a .Net SDK application is talking to a Workgroup Server on the same machine you should use 'local' for the Workgroup Server name, rather than using the machine name. Why is this? Because 'local' will use a named pipes connection to the Workgroup Server rather than going through the network layer.
Web Client / ServiceAPI Users read this!
Of course the Web Client, ServiceaAPI and WebDrawer are .Net SDK applications, so of you are connecting to a WorkGroup Server on the same machine use 'local'. Note the MSI will probably not suggest this to you so you wil have to edit the hptrim.config manually. Make it look something like this:
<workgroupServer port="1137" workPath="C:\HP Records Manager\ServiceAPIWorkpath" name="local" alternateName="MY_ALTERNATE_WGS" alternatePort="1137" />
Hello, World!
.Net SDK Developers
If you are writing an application to run on the same machine as the Workgroup Server do something like this:
using (Database database = new Database()) { database.Id = "J1"; database.WorkgroupServerName = "local"; database.Connect();
ServiceAPI - page through the entire result set
There are a few parameters that assist you in paging through an entire pageSet. If you have a good connection to the workgroup server (e.g. server to server) then make the pageSize larger rather than smaller. The response property HasMoreItems will return false when you are finished. The sample below assumes basic authentication, check out the ServiceAPI documentation for other authentication methods.
Sample
TrimClient trimClient = new TrimClient("http://localhost/ServiceAPI"); trimClient.AlwaysSendBasicAuthHeader = true; trimClient.UserName = "MY_USER_NAME"; trimClient.Password = "MY_PASSWORD";
Create a new Record version in the ServiceAPI
There is a sample for creating a new version of a Record in the ServiceAPI help, found here: /examples/RecordNewVersion in your ServiceAPI help. This sample uses a standard form post. The technique using the .Net wrapper classes is very similar. The process is:
Example
Record record = new Record(); record.Uri = 9000008003; record.NewType = NewType.Version; // this tells the request I want the Record Number property in the response. record.Properties = new List<string>() { "RecordNumber" };
Web Service performance tips
Whether you are using the ServiceAPI or your own web service built on the .Net SDK there a re a couple of things you can do to improve performance.
Object Cache
The object cache allows you to cache Records (and other objects) in memory, this removes the requirements to connect to the database next time this object is requested. This, is of course, most useful when certain Records are requested more than once. You can increase this to up to 99,999 for server applications (see below).
ServiceAPI searching - reserved characters
I caught myself out with this today. There are two characters used in searching that conflict with URL reserved characters, this is the less than (<) and plus sign (+). I tried to do a recursive member search (members:[default:Adelaide]+) and found the recursiveness not happening. ServiceAPI works around this limitation by replacing < and + with non-reserved characters. &Lt replaces < and ~ replaces +. So my search should have been members:[default:Adelaide]~.
Message strings (Localisation)
The .Net SDK and the ServiceAPI allow you access to many of the localized strings used by RM. To fetch a list of messages from the ServiceAPI send a regular expression containing all the message ids you are interested in to the Localisation service.
http://localhost/ServiceAPI/Localisation?format=json&MatchMessages=(^vb_tio_Help$|^vb_tio_OK|^vb_tio_Cancel$|^vb_tio_MessageFormat$|^vb_TIO_RecordMetaData$)
Note in the ServiceAPI request above:
WebDrawer - excluding records
WebDrawer (and the ServiceAPI) allow you to exclude Records (and other object types) from search results. The trick is to find (or add) a routeDefault in hptrim.config. In WebDrawer the record search already has defaults set in hptrim.config so you simply need to go to the routeDefaults elements, find the ADD element matching the one below and add the 'q' attribute.
In the samples below the 'q' parameter will be added to every Record search.
In this example I only want to include electronic documents.
<add name="Record" model="Records" template="WDRecordList" q="electronic" properties="RecordRecordType,RecordExtension,RecordTitle,RecordNumber,RecordIsElectronic" />
In this sample I only want non-electronic documents.
<add name="Record" model="Records" template="WDRecordList" q="not electronic" properties="RecordRecordType,RecordExtension,RecordTitle,RecordNumber,RecordIsElectronic" />
NOTE: The 'model' in the routeDefault above indicates that the elelement applies to record search results. A model of 'Locations' would apply to location search results.
More help: Route defaults are discussed in the online help shipped with the ServiceAPI under /help/routing.
Javascript multi file upload
Overview
There are a number of ways to do a multi file upload, this is a simple one that has these features:
Tutorial
The setup
In hptrim.config:
<add name="Record" model="Record" />
The code
Copy this file to your WebDrawer root file (or a subfolder if you wish).
New in 83 - ServiceAPI batch updates
In 83 we have a new capability to perform the same operations that you could always perform on individual objects on sets of objects.
Example
Posting the following to the record endpoint will add a jurisdiction to both records REC_307 and REC_308
{ "Q":{"q":"number:REC_307,REC_308"}, "ChildJurisdictions":{ "RecordJurisdictionJurisdiction":"9000000007" } }
New in 83 - Custom Property help page
A simple but hopefully useful addition for those who use the ServiceAPI.
Uploading files in RM 83
We have always been able to upload a new revision for a record, uploading other document types (e.g. record rendition, location electronic attachment) was not always that simple, now it is.
ADFS claim rule mapping
Map User-Principal-Name to Name
This is a for those of you who notice a gap in the current RM Web Client ADFS configuration documentation. The documentation for the RM native client specifies the Relying Party claim rule should have the LDAP attribute User-Principle-Name mapped to the outgoing claim UPN. The Web Client documentation contains a simple custom rule but does not specify exactly which claims are are required. Unlike the native client the User-Principal-Name must be mapped to the outgoing claim Name, like this:
Improve TrimMainObjectSearch with LimitOnRowsReturned
Update
Jan comments that LimitOnRowsReturned adversely affects FastCount, the good news is that this is fixed in 82. On the topic of FastCount there is a fix in the pipeline to improve its performance, at the moment if you call FastCount the SDK will trigger an extra, redundant, SELECT. It is not always possible but I avoid FastCount where I can.
TrimMainObjectSearch may be used in a number of ways, sometimes to return all records matching a particular search criteria, other times (for example when supporting a user interface) to return only the first N records matching the search criteria. If it is only the first N records you are interested in then use LimitOnRowsReturned (and PagingMode) to generate a more efficient SQL SELECT statement.
ServiceAPI - Attaching keywords
Overview
Keywords (otherwise known as Thesaurus terms) use the child collection pattern to establish a relationship with a record, this is similar to other related objects such as holds and locations. One difference with keywords is that you cannot use the standard pattern of adding them to the child collection.
A fix in an upcoming release
Unfortunately existing releases (up to and including 8.2) do not throw an exception to tell you that the adding of the keyword to the ChildKeywords collection has not worked, they simply fail. You should expect that an upcoming release will actually throw an exception and give you a clue how you proceed.
So, how should you proceed?
In 8.2 a keyword is added using the AttachKeyword service action, using the .Net client classes you would do something like this:
TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI82"); trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;
ServiceAPI file upload with date created
Intro
Today's question was how to upload a file including the date created. One way to do it would be to transfer the file using some other process to the ServiceAPI server and then use the RecordFilePath property attach it to the record. The other way is to send the RecordDateCreated property with the form post.
The code
This sample can be installed by copying it into the ServiceAPI examples folder and then opening it like this: http://localhost/HPRMServiceAPI/examples/CreateWithDateCreated.
What it does
The sample contains more detailed description but in short it:
Record creation with ACL in the ServiceAPI
I had a question the other day about creating a new record and setting the ACL at the same time. This led me to realise that the only sample for this in the ServiceAPI documentation was quite complex (and only for record update not creation). In an attempt to remedy this I have added a new sample. To use this copy the file into your ServiceAPI 'examples' folder and then open the URL: http://<myServer>/HPRMServiceAPI/examples/CreateWithACL_JSON.
Code
The actual code simply composes a JSON object to represent the new record and includes an AccessControlList property containing a valid ACL object. This object is documented in the ServiceAPI documentation. The JSON that is posted looks like this:
var acl = { "FunctionEnum": "RecordAccess", "FunctionProfiles": { "DestroyRecord": { "Setting": "Private", "ReferenceStyle": "NoRefNoCopy", "AccessLocations": [ { "FindBy": "me" } ] } } }
ADFS client side authentication (part 2)
Demo
This demonstrates the creation of a simple console application which uses ADFS to authenticate and then passes those credentials to the ServiceAPI. If you are interested in ADAL then read Vittorio Bertocci's blog, particularly this post. The token cache I use in the below video can be found here, the code I wrote below is here.
Refresh tokens
A refresh token will allow a client to keep their credentials cached for days rather than hours however in my experience ADFS does not issue refresh tokens by default. You may wish to do some research on refresh tokens and decide whether or not you want to support them. If yes then use the following powershel command to enable them for your relying party trust. You can choose to issue refresh tokens to AllDevices or WorkplaceJoinedDevices.
Set-AdfsRelyingPartyTrust -TargetName "davidc2012 ServiceAPI" -IssueOAuthRefreshTokensTo AllDevices
ADFS client side authentication (part 1)
Note
In version 8.3 and later it is not required that you update the ServiceAPI itself to support OAuth as it will support it by default. As long as you send the Bearer token in the Authentication header and set up the <authentication> element in hptrim.config.
Demo
In this video I configure the ServiceAPI to force it to use my ADFS instance for authentication for client side applications. This is achieved by using the OWIN framework to enable OAuth2 in the ServiceAPI instance. I also look briefly at what is required on the ADFS side to make all this work.
The Code
Here are the resources I used in the video:
The command I used to create the ADFS client:
Add-ADFSClient -Name "MySAPIClient" -ClientId "A1CF1107-FF90-4228-93BF-26052DD2C714" -RedirectUri "https://davidc2012.trim.lab/HPRMServiceAPI/"
ServiceAPI - Attach an action
Background
Today's question of the day from the forums is how to attach an action from the ServiceAPI. Took me a few minutes to work out exactly what was going on here as the ServiceAPI inherits a little bit of the opacity of the SDK.
The Code
TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI"); trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;
Paging issues in the web client
Background
Somewhere lost in the mists of time (HPRM 8.1.x or so) there was an issue in the HPRM web client which caused the second page of your search results to sometimes contain results that you had already seen in the first page. We fixed this in a patch of course.
But wait!
Upgrading to the latest patch is not always achievable, maybe, just maybe there is a work-around? Turns out there is, try putting this inside the routeDefaults element of your hprmServiceAPI.config file:
<add name="Record" model="Records" sortBy="recRegisteredOn- unkUri" includePropertyDefs="true" />
What is going on here? The sort by Uri puts the search results in a predictable and static sequence, the sort by date registered puts them in a useful sequence.
Not only a work-around
Given that the Web Client does not yet support custom sorting you may wish to use this technique to sort search results by something more meaningful than the default.
How to create a communication
Background
A common activity around a record is to send and receive communications in relation to it, be they physical letters or emails. If you want an audit trail of communications sent and received you may be interested in the communication object.
Create a communication
The code below uses the ServiceAPI .Net client library to create a new communication. Some points to note are:
TrimClient trimClient = new TrimClient("http://david-pc/ServiceAPI"); trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials; Communication communication = new Communication(); communication.Record = new RecordRef() { Uri = 9000000018 }; communication.Direction = CommunicationDirection.Outgoing;
Today's question - creating record relationships from the ServiceAPI
Background
In HPRM records may have a variety of different relationships, from 'Redaction of' to 'Related to'. These relationships are managed in both the SDK and ServiceAPI via the child collection construct. To add a relationship you add a new item to the ChildRelationships collection, to remove a relationship you remove an item from this collection.
C#
This code will create a new relationship using the .Net client for the ServiceAPI.
TrimClient trimClient = new TrimClient("http://MyHost]/ServiceAPI"); trimClient.Credentials = System.Net.CredentialCache.DefaultCredentials;
Thumbnails
Intro
This post pulls together techniques discussed in previous posts to examine how to auto generate thumbnails for images, store them as rendtitions, expose them via the ServiceAPI and display them in WebDrawer.
Warning: ServiceAPI plugin feature featured in this post requires HPRM 8.1.1 or later.
The talking
Links
Code available here:
Related Posts
Some related posts to get started:
Remove page numbers to make WebDrawer faster
Intro
I have previously discussed ExcludeCount and search result page numbers and how we can improve WebDrawer search performance, especially for large databases. In HPRM 82 this is all much simpler.
How to
WebDrawer 82 will adjust its paging model to use back and next, rather than numbered pages, if the search routes are set to show results only. To do this you need to add resultsOnly="true" to the two record search routes in hptrim.config. For example:
<add name="FormSearch" model="FormSearch" resultsOnly="true"/>
Fun with 82 - Field search clause names
This particular feature (or bug fix, depending on your perspective) may not look all that big or exciting but it made a big difference to my life. Life is better when we limit the 'garbage in' to any system!
ServiceAPI permissions are null
Intro
One of the nice things about a service oriented approach is that, in theory, you can upgrade the server and all of the existing clients continue to work, simply ignoring any new features on the server. A post on the HPRM SDK forum highlighted a bug in our ServiceAPI .Net client which prevented this from working fully.
The problem
The Location object has a permissions property. This is a dictionary with the UserPermissions enum as the key. Unfortunately the serializer fails to deserialize the entire dictionary if one of the keys is invalid. This means that if we add new items to UserPermissions (which we do) then Location.Permissions will be null when de-serialized by an earlier version of the ServiceAPI .Net client library.
The solution
An upcoming release of the ServiceAPI will fix this. In the meantime, if you wish to future proof your application, you can patch the .Net client library yourself. Simply call the code below at the start of your program (maybe from a static constructor to ensure it is only called once.
JsConfig<PermissionsDictionary>.RawDeserializeFn = (val) => { PermissionsDictionary permissions = new PermissionsDictionary();
Fun with 82 - Tracing errors
In HPRM 82 we have a new feature that allows you to write a trace of the request that caused an error. This feature should only be enabled if you are getting errors, for two reasons:
Thumbnail Views (a response for Brian)
A comment
Someone called Brian posted a comment on one of my posts a month ago, unfortunately I never noticed this comment. I find the best place for actual conversations to be the various HP RM Discussion boards, I monitor them daily but rarely look at comments on this blog. Just in case Brian is reading I will answer his question here.
The question
Do you think it would be possible, and effective, to leverage the preview capability to display a thumbnail grid of search results?
My answer
I have thought about this on a number of occasions and my conclusion is no. The preview is slow and does not have a facility to generate a preview of a single page. I do not see it as a solution for a thumbnails view.
Another answer
If what you are interested in is thumbnails of images (PNG, GIF, JPG, etc but not other document types) and you are happy to do your search in WebDrawer (not the Web Client) then there is a potential solution. I have previously supplied a sample EventProcessor add-in to auto create a thumbnail rendition of each image document as it is created/modified. It is quite simple then to have a custom search view in WebDrawer to display this thumbnail rendition.
So...
I will come back and check this post in a week or so, if you really want the samples I describe above add a comment. Or put in a request on one of the discussion boards. I cannot make any promises as compiling samples has to fit in around other work but I will do my best.
Google authentication
Introductory thoughts
The ServiceAPI (upon which WebDrawer and the Web Client are based) has a pluggable authentication module. This allows for the support of a variety of identity providers (IdP). For those considering Google as IdP this is the first step in that direction.
Breaking News!
While doing some testing in 8.2 I found a regression that means plugins will not work in the 82 Web Client (WebDrawer and ServiceAPI or OK). This should be fixed in the first 82 patch. So be warned, 811 should be fine but not 82, skip straight to 8.2 Patch 1 if you want to use Google authentication in the Web Client.
Looks simple to me
In which I demonstrate the process to:
Secrets
You may note I do not make an effort to obscure the various bits of secret information in my Google Developer console. Don't worry, I have re-set the sensitive bits.
Files
Available to download are the:
HPRM 82
The code remains the same but in HPRM 82 some interfaces have changed, use these for 82 instances:
Source of the source
For those interested the source for the above plugin all comes from ServiceStack framework (V3), look in the ServiceStack.Authentication.OAuth2 project and ServiceStack.ServiceInterface/Auth.
Steps
The steps I follow in the video above include:
Create a Google Client Id
Go to console.developers.google.com/project to create a project then create a client id for a web application
Edit the config files
In the WebDrawer web.config I add this in the appSettings element, making sure to use the correct WebDrawer URL of course:
<add key="oauth.RedirectUrl" value="https://811x.hprm.info/GWebDrawer/"/> <add key="oauth.CallbackUrl" value="https://811x.hprm.info/GWebDrawer/auth/{0}"/> <add key="oauth.GoogleOAuth.ConsumerKey" value="YOUR_CLIENT_ID"/> <add key="oauth.GoogleOAuth.ConsumerSecret" value="YOUR_SECRET"/>
In hptrim.config I add this:
<pluginAssemblies> <add name="GoogleAuthPlugin" /> </pluginAssemblies>
Copy the DLLs
Lastly I copy the DLLs into the WebDrawer\bin folder.
For the Web Client only
The Web Client embeds the ServiceAPI slightly differently to WebDrawer, for this reason you will need to add a login element to appSettings (in web.config), just like this one.
<add key="oauth.login" value="~/HPRMServiceAPI/auth/GoogleOAuth" />
Using Postman to experiment with the ServiceAPI
Introduction
The ServiceAPI includes a number of facilities for experimenting with service calls, including extensive samples and the 'swagger' interface. If you want to test your service requests in a less structured way without having to write code then Postman is a great option.
The video
This video is a brief introduction to using Postman demonstrating how to do a simple Record search.
Simple document download
Background
In a previous post I showed how to patch the ServiceAPI TrimClient to fix a problem with downloading child documents (e.g. record revisions). A customer (Mark from WA) pointed out a way to download a file to a stream without having to write it to a file.
Examples
The following code downloads a document using a stream. I then save to a file but it could also be passed on as a stream or byte array to another system.
Stream stream = trimClient.Get<Stream>(string.Format("Record/{0}/File/Document", 1843)); using (FileStream fs = File.Create("d:\\junk\\myfile.xml")) { stream.CopyTo(fs); }
Below is the code to use to get a record revision as a stream.
Stream stream = trimClient.Get<Stream>(string.Format("Record/{0}/RecordRevision/{1}", 1843, 12));
Side benefits
Two side benefits of this approach are:
Download a child document using the .Net client
Background
Today I fielded a request for help downloading a Record Revision using the ServiceAPI .Net client. Thinking this should be simple I prepared the sample code below, only to find it failed. Similar sample code works when downloading a document. Turns out we have a bug.
ChildDownload request = new ChildDownload(); request.ChildUri = 12; request.Id = "1843"; request.TrimType = BaseObjectTypes.Record; request.TrimChildType = BaseObjectTypes.RecordRevision;
Deleting Records in the ServiceAPI from .Net
Background
Unfortunately deleting a Record (or other object type) is slightly different to other ServiceAPI requests. This is both because most requests assume a response and also because the delete service is not explicitly built into the .Net client.
Two ways to delete
Each HPRM object has a Delete service which can be posted to via a URL following this pattern: /Record/{Id}/Delete. Given that the .Net client classes do not have a facility to post to a specified URL you would need to write your own code to post to this URL.
The second way to delete is to post the deletion request to one of the built-in service endpoints. This can be done like this:
trimClient.Post((new DeleteRecord() { Id = 1837.ToString(), DeleteContents = true });
Or this:
trimClient.Post(new DeleteMainObject() { TrimType = BaseObjectTypes.Record, Id = "1838" });
The catch
Not every version of the ServiceAPI enables the built-in service endpoints by default. They can be switched on by adding PreDefinedRoutes to the serviceFeatures in the hptrim.config file.
<hptrim poolSize="1000" serviceFeatures="Json,PreDefinedRoutes,Razor,Html" ... >
How do I create a record relationship?
Intro
There is a lot going on in HPRM and it is not always apparent where to go in the ServiceAPI to do what you want to do. In this video I have brief look around as I work out how to add a record relationship.
C# Example
This example uses the c# libraries as documented at this path /help/dotnetclient in your ServiceAPI to add a new child relationship to the ChildRelationships collection.
TrimClient trimClient = new TrimClient("http://myserver/HPRMServiceAPI"); trimClient.Credentials = new NetworkCredential("david", "XXXXX"); trimClient.AlwaysSendBasicAuthHeader = true;
ExcludeCount and performance
Background
I am doing some load testing on an HPRM dataset containing 16 million records. Response times from our web clients were not great, depending on which search query I used of course. I really wanted to improve this.
ExcludeCount
A ServiceAPI parameter not often mentioned is ExcludeCount (otherwise known as resultsOnly). This has the effect of committing the SearchTitle and TotalResults properties from the response. Total results in particular can degrade performance on a large dataset as it calculates the total number of records matched by the query.
ServiceAPI Example
http://[myserver]/HPRMServiceAPI/Record?q=uri:2013778,1208882,1380177,1542060,1378119&format=json&Excludecount=true
WebDrawer Example
Setting ExcludeCount as the default in WebDrawer will result in the search results page no longer displaying the search title. To do this add the 'resultsOnly' attribute in the Record route defaults, as seen below. Ensure you set this on the route named Record with the model named Records.
<add name="Record" model="Records" template="WDRecordList" properties="RecordRecordType,RecordExtension,RecordTitle,RecordNumber" pageSize="15" resultsOnly="true"/>
The outcome
On my large dataset ExcludeCount more than halved the response time on many requests.
The Cost
In WebDrawer the cost is that the page number links no longer work. This post examines a solution to the broken paging problem.
ServiceAPI Plugin Example
Why?
The 811 ServiceAPI documentation contains a very simple example of a ServiceAPI plugin. This example takes it one step forward and demonstrates hows to participate in the ServiceAPI pipeline to get a database connection for the current user.
Your Service
The service below inherits from the class TrimServiceBase which provides you with the property called 'Database'. This is the best way to get a database connection for the current user as it uses the same mechanism to acquire (and release) a connection as used by every other service in the ServiceAPI.
namespace ServiceAPIPlugin {
Customise the WebDrawer HTML preview page
Update!
The solution I provide to display the native PDF in the preview page is flawed. A better solution is available here.
The problem
What if the HTML preview of a document is not displaying as you think it should? Depending on the cause this may be out of our hands, however some problems can be avoided even if they cannot be completely resolved.
An Example
This particular post was spurred by one customer who was experiencing strange black boxes overlaying the HTML rendition of some PDFs. It turns our that this was a problem in the rendered HTML being exaggerated by the way the HTML was displayed in WebDrawer.
A Fix
In this video I spend the first half improving the viewing experience by displaying the HTML preview within an iframe, at about the 13 minute mark I look at how you might simply embed the native PDF in the preview page rather than rely on the HTML rendition of the PDF.
Sorry about the audio levels in this, you may have to turn your volume up to full and listen carefully...
Scripts
This is the scripts section as seen in the above video. It sets the iframe:
@section scripts { <script> var resizePreview = function () { $('#preview-frame').show(); $('#preview-frame').css("visibility", "visible");
Fetch a list of record revisions and display them in WebDrawer
Intro
This video goes start to finish fetching a list of record revisions and creating a list in WebDrawer. Along the way I show where in the ServiceAPI help I found all the information I needed to achieve this.
Sample Code
In this video I call the ServiceAPI with a URL like the one below to fetch a list of record revisions.
http://localhost/HPRMServiceAPI/Record/REC_15?properties=ChildRevisions
I write this Razor code:
<ul> @foreach (RecordRevision revision in this.Model.Results[0].ChildRevisions) { <li><a href="~/Record/@this.Model.Results[0].Uri/RecordRevision/@revision.Uri">@revision.Description</a></li> } </ul>
Modify a routeDefault to look like this:
<add name="Record" model="RecordFind" template="MyRecordDetails" properties="NameString,RecordIsElectronic,enabledcommandids,RecordRelatedRecs,RecordIsContainer,RecordIsInPartSeries,RecordRecordType,RecordPrimaryContact,RecordExtension,ChildRevisions" propertySets="Identification,ChildDetails"/>
Add this custom property Set:
<add name="RecordRevisionChildDetails" propertySets="All" properties="RecordRevisionDescription,NameString"/>
Notes
The custom property set I added above has redundant information. I specify the propertySet 'all' which means that I do not need to specify the individual properties at all.
Also, you might notice a record revision property in the help that has a numeric ID (in fact I comment on this). This was the result of me working with a partially re-built instance of HPRM.
And lastly, if you see some odd looking things in WebDrawer (such as the 'Create' button) just ignore them. They are a part of some other sample code I am currently part way through building.
Access properties of object properties in a WebDrawer template
Intro
Some things in the Razor templates of the ServiceAPI and WebDrawer are not all that obvious, one is how to display the name of the record author. In an earlier post I created a custom search form, this video shows a custom search results template and how to display the name (or any other property) of the record author.
A Future Release...
In a future release accessing properties of property objects should become a little more straightforward.
Optimising the performance of the WebDrawer details page
Intro
WebDrawer is designed to show more rather than less of what a user may wish to see. Being a simple HTML application it does not aim to do anything as sophisticated as on demand loading of data. The cost of this approach is that a Record details page contains a lot of information and can be quite slow to load. If your users typically do not need to see all of this information then you can remove it and improve the load time of this page significantly.
Try It Out
This video demonstrates removing the bulk of the properties displayed on a Record details page and improves load of the page from around 4 seconds to just over 1 second. I hope you enjoy the soothing sounds of children playing in the background on this video.
Make it more maintainable
While the above will improve the performance of the Record details page it will make it more difficult to upgrade WebDrawer. One way to improve this situation is to not edit the Razor files that ship with WebDrawer but to copy them and use your own. This way your changes will not be overwritten by an upgrade. In the picture below I have created a folder called 'MyViews' within which I have made a copy of WDRecordDetails.cshtml (renamed MyRecordDetails.cshtml) and a shared folder in which I have a copy of detailsView.cshtml (renamed MyDetailsView.cshtml). To use the MyRecordDetails View I had to specify it in the 'template' property of the routeDefault element in hptrim.config.