Enhanced Diagnostic display#
A useful way to explore the features of the Display API is to create a diagnostic display to exercise various aspects of the API and log information when interacting with the display and view the results.
Note
The code for this tutorial can be reviewed at Tutorials/EnhancedDisplayPlugin
With the ability to log messages, we can start to explore the API by overriding each of the View Model virtual methods and then experiment with the display in ATLAS to see when and in what situations these overridden methods are called.
For state, methods and services we can explore these by adding additional user interface elements to the View to view the results and any side effects of interacting with them.
Start the tutorial by creating a new display from scratch named EnhancedDisplayPlugin
.
Add logging#
Modify the XAML of the View to contain a TextBox
to view the diagnostic log
<UserControl x:Class="EnhancedDisplayPlugin.SampleDisplayView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<TextBox VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
IsReadOnly="True"
Text="{Binding LogText, Mode=TwoWay}">
</TextBox>
</Grid>
</UserControl>
Add a string
property named LogText
to the View Model (see Display Properties for further information)
[Browsable(false)]
public string LogText
{
get => this.logText;
set => this.SetProperty(ref this.logText, value);
}
Add the following backers
SynchronizationContext
field namedsynchronizationContext
to update theTextBox
on the UI threadList<string>
field namedpendingLogMessages
to hold pending log messagesobject
field namedlogLock
to prevent multiple threads accessing the log at the same time
private readonly SynchronizationContext synchronizationContext;
private readonly List<string> pendingLogMessages = new List<string>();
private readonly object logLock = new object();
Set the SynchronizationContext
field in the View Model constructor.
public SampleDisplayViewModel()
{
this.synchronizationContext = SynchronizationContext.Current;
}
Attention
Notification methods and signal result handlers may be called concurrently on separate threads, therefore some form of thread synchronization is required when accessing shared resources.
User Interface must be updated on the UI thread, the SynchronizationContext
class is used in this tutorial.
Add a helper method named LogMessage
to update the log
private static void LogMessage(
SynchronizationContext synchronizationContext,
ICollection<string> pendingMessages,
object logLock,
Func<string> readLog,
Action<string> writeLog,
string message)
{
bool firstNewMessage;
lock (logLock)
{
firstNewMessage = !pendingMessages.Any();
pendingMessages.Add(message);
}
if (firstNewMessage)
{
synchronizationContext.Post(
_ =>
{
lock (logLock)
{
var stringBuilder = new StringBuilder(readLog());
foreach (var pendingMessage in pendingMessages)
{
stringBuilder.AppendLine(pendingMessage);
}
pendingMessages.Clear();
writeLog(stringBuilder.ToString());
}
},
null);
}
}
Finally add a simplified method named Log
to log a single message
private void Log(string message)
{
LogMessage(
this.synchronizationContext,
this.pendingLogMessages,
this.logLock,
() => this.LogText,
v => this.LogText = v,
message);
}
Exploring state#
The API state is the value of any properties exposed by the View Model and any properties obtained from services, e.g. configured parameters or session summary.
To view the API state at any time add a Button
to the View XAML
<Button Command="{Binding LogPropertiesCommand}">Log Properties</Button>
Note
Layout the User Interface using an appropriate WPF panel, such as: Grid
, StackPanel
or WrapPanel
.
Add a command handler named LogPropertiesCommand
to the View Model
[Browsable(false)]
public ICommand LogPropertiesCommand { get; }
Initialise the LogPropertiesCommand
command handler in the View Model constructor
this.LogPropertiesCommand = new DelegateCommand(this.OnLogProperties);
Implement the command handler by logging appropriate property values
private void OnLogProperties()
{
this.Log(nameof(OnLogProperties));
this.Log($" IsSelected: {this.IsSelected}");
this.Log($" CanRetrieveData: {this.CanRetrieveData}");
this.Log($" ScopeIdentity.Identity: {this.ScopeIdentity.Identity}");
// Log this.ActiveCompositeSessionContainer
// Log this.displayParameterService.ParameterContainers
Note
The code for this tutorial Tutorials/EnhancedDisplayPlugin provides a full implementation of OnLogProperties
.
The code also allows you to log the properties periodically, this is useful to demonstrate how the API state changes whilst the display is non-interactive, e.g. covered by another display or a different page is active.
Exploring notifications#
Notification methods are virtual methods exposed from the View Model and are called when specific events occur in ATLAS, e.g. when the active page is changed.
Override notification methods and add log statements to view the value of any arguments, e.g.
public override void OnActiveDisplayPageChanged(bool isActive)
{
this.Log(nameof(OnActiveDisplayPageChanged));
this.Log($" IsActive: {isActive}");
}
Add overrides for all the notification methods as detailed in the following sections
Initialization and state change notifications#
OnInitialised()
OnActiveDisplayPageChanged()
OnCanRenderDisplayChanged()
Session notifications#
OnCompositeSessionLoaded()
OnCompositeSessionUnLoaded()
OnCompositeSessionContainerChanged()
Display Parameter notifications#
OnParameterContainerAdded()
OnParameterContainerRemoved()
OnParameterAdded()
OnParameterRemoved()
Timebase and cursor notifications#
OnCursorDataPointChanged(ICompositeSession)
OnSessionTimeRangeChanged(ICompositeSession)
Exploring timebase/cursor#
Using ISessionCursorService
, it is possible to set the cursor timestamp by calling MoveCursor()
To gain access to ISessionCursorService
, add a backer and inject an instance into the View Model constructor
private readonly ISessionCursorService sessionCursorService;
public SampleDisplayViewModel(ISessionCursorService sessionCursorService)
{
this.sessionCursorService = sessionCursorService;
}
Note
See Factories and Services for an explanation of other services
Add a Button
to the View XAML
<Button Command="{Binding CentreCursorCommand}">Centre Cursor</Button>
Add a command handler named CentreCursorCommand
[Browsable(false)]
public ICommand CentreCursorCommand { get; }
Initialise the CentreCursorCommand
command handler in the View Model constructor
this.CentreCursorCommand = new DelegateCommand(this.OnCentreCursor);
Implement the command handler by calculating the timestamp at the centre of the current timebase and passing to MoveCursor()
private void OnCentreCursor()
{
if (!this.CanRetrieveData ||
!this.ActiveCompositeSessionContainer.IsPrimaryCompositeSessionAvailable)
{
return;
}
var primarySession = this.ActiveCompositeSessionContainer.CompositeSessions.FirstOrDefault(c => c.IsPrimary);
if (primarySession != null)
{
var centreTimestamp = (primarySession.TimebaseRange.Start + primarySession.TimebaseRange.End) / 2;
this.sessionCursorService.MoveCursor(primarySession, centreTimestamp);
}
}
Exploring data requests#
Data Requests can be explored by logging the value of parameters when the cursor is changed.
Ensure display parameters are allowed by adding the DisplayPluginSettings attribute to the View Model
[DisplayPluginSettings(ParametersMaxCount = 100)]
public sealed class SampleDisplayViewModel : DisplayPluginViewModel
Add backers for the required services, then inject and initialize
private readonly ISignalBus signalBus;
private readonly IDataRequestSignalFactory dataRequestSignalFactory;
private IDisplayParameterService displayParameterService;
public SampleDisplayViewModel(
ISignalBus signalBus,
IDataRequestSignalFactory dataRequestSignalFactory)
{
this.signalBus = signalBus;
this.dataRequestSignalFactory = dataRequestSignalFactory;
}
protected override void OnInitialised()
{
this.displayParameterService = this.ServiceContext.DisplayParameterService;
}
Register with the SignalBus
a data result handler for sample requests to the View Model constructor
this.signalBus.Subscribe<SampleResultSignal>(
this.HandleSampleResultSignal,
r => r.SourceId == this.ScopeIdentity.Guid)
Note
The result from calling Subscribe
should be properly disposed, the tutorial code demonstrates this.
Make a sample data request for each display parameter when the cursor changes
public override void OnCursorDataPointChanged(ICompositeSession compositeSession)
{
if (!this.CanRetrieveData)
{
return;
}
foreach (var primaryParameter in this.displayParameterService.PrimaryParameters)
{
var signal = this.dataRequestSignalFactory.CreateSampleRequestSignal(
this.ScopeIdentity.Guid,
compositeSession.Key,
primaryParameter,
compositeSession.CursorPoint + 1,
1,
SampleDirection.Previous);
this.signalBus.Send(signal);
}
}
Hint
Since there may not be an exact sample value at the cursor timestamp, it is best practice to +1 to the timestamp and request the previous sample value.
Log the sample value for the the display parameter if valid
private void HandleSampleResultSignal(SampleResultSignal signal)
{
var request = signal.Data.Request;
var result = signal.Data;
var parameterValues = result.ParameterValues;
if (parameterValues.SampleCount == 1)
{
this.Log(nameof(HandleSampleResultSignal));
this.Log($" Parameter: {request.Parameter.Identifier}, Value: {parameterValues.Data[0]}");
}
}
Caution
Due to reuse of the arrays contained within ParameterValues
, the Length
property of those arrays should not be used.
- Use the
SampleCount
property to determine the extent of the returned values. - If you wish to retain a copy of
ParameterValues
you must callLock()
, and thenUnlock()
to release the reference when no longer required
Testing the display#
To view the parameter values
- Add a session via the Session Browser to the compare set associated with the display
- Add some display parameters to the display via the Parameter Browser
- Use a Waveform display to change the cursor
Other API features#
The tutorial code also demonstrates the following API features
- Working with Display Properties
- Demonstrates adding and handling properties within the the properties window
- Tracking timebase changes and logging the minimum and maximum values of configured parameters
- Demonstrates issuing and handling data requests between two timestamps