Skip to content

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 named synchronizationContext to update the TextBox on the UI thread
  • List<string> field named pendingLogMessages to hold pending log messages
  • object field named logLock 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 call Lock(), and then Unlock() 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