USER IO in simple (example) test GUI?

While the dialog steps work well in the editor, the example GUI does not implement the user interface. Using breakpoints I see the dialog boxes call the routines in the GUI but pass a type “object” for which the parameters vary (or don’t exist) depending on how the user interface call was generated. The basic dialog example passes parameters, but the SN dialog example does not.

Is there an example of how to typecase the “object” to a meaningful dialog structure if the .toString shows that it is defined by the Basic dialog?

I assume wjat I’m looking for is: OpenTAP.TUI/TuiUserInput.cs · master · OpenTAP / Plugins / Keysight Technologies / OpenTAP-TUI · GitLab

1 Like

Hi @craig.petku,

I would strongly suggest looking to the TUI for inspiration. Most things you could want to implement in an OpenTAP test plan editor and runner can be found there.

This part is probably the most important:

 var annotations = AnnotationCollection.Annotate(dataObject);
 var members = annotations?.Get<IMembersAnnotation>()?.Members?.ToList();

AnnotationCollection is the class we use to display and edit data. In many cases it is enough to support IStringValueAnnotation.

IStringValueAnnoations can be found on properties that can be edited and viewed as strings. For example numbers.

IAvailableValueAnnotationProxy can be found on propeties that can be edited and viewed as a drop down.

Many others exists, but the TUI implements support for most things.

2 Likes

Thank you. I tried building TUI from source so I could then understand how to integrate it into the example UI. The problem I’m having is the source package references terminal.gui . I finally realized it might be a nuget package and installed the latest version (1.41). While this solved a lot of compiler issues, the TUI package still doesn’t compile . The best description is in LogPanelView.cs, the Redraw override uses variables that are not found. These include top and selected. Any ideas? I’m leaning towards it being the missing Terminal.gui project (source) referenced by TUI.

So looks like what you have to do is clone that project as well: StefanHolst/gui.cs at main (github.com) into the gui.cs folder. I also had to clone it manually to get it to work, and make sure you checkout the “main” branch (“master” branch is still default but “main” branch is current). Then it should compile.

Thank you. Spent the better part of the day trying to figure this out. Had found the repository but was using Master (didn’t work.

TUI is now compiling. Now I have to change something to ensure what’s running is the version I compiled.

Yes that’s another unfortunate part… There isn’t any debug launch configuration. Solution is go to the Properties\launchSettings.json file and make it look like this:

{
  "profiles": {
    "OpenTAP.TUI": {
      "commandName": "Executable",
      "commandLineArgs": "tui",
      "executablePath": ".\\tap.exe"
    }
  }
}

Thanks,

I typically just handle this with a command line parameter in a shortcut to tap.exe. I verified it was running the compiled version by changing a title in one of the windows. Now I can move on to porting this working version to my custom UI.

While TUI is a fair way to avoid licensing the editor, neither have a good interface for showing results and latched pass/fail status for each monitored DUT. I’m handling this in a custom results listener which displays the data in a matrix and color codes the cells for pass/fail. I also use the CSV plugin to limit file sizes since tests can run for several days collecting data on 1-2 second intervals.

What has worked well for visualizing the monitored parameters in real time is Grafana (another open source tool). It has a plugin for reading CSV files and does a good job graphing data. Since it can update at 5 second intervals, trends over temperature are easy to spot during pre-validation tests. I would argue it’s significantly better than the results viewer since I can share data files and analysis templates with others then let them focus on what they want to look at. Editor still remains the prefered tool for creating/debugging test sequences.

1 Like

Sure, what you’re saying makes sense. The Results Viewer is I think just general, but the real power in OpenTAP is the flexibility and extensibility, like being able to create custom Results Listeners and utilize other tools like the one you used.

1 Like

FYI, gui.cs is a submodule in the TUI project. The normal way of fetching this is:

git submodule update --init --recursive   #first time only
git pull --recurse-submodules
2 Likes

Very good to know, thanks Rolf. Haven’t had the need before for this, and didn’t realize you could do it

1 Like

I think it is not the most intuitive part of git. I have to look it up every time I need to use it…

As for the launchSettings.json, I think that this is very IDE specific. VS, VS Code and Rider all have different ways of doing it. I’m sure @stefan.holst will accept a merge request if you have something that works generally.

1 Like

Hmm… I hadn’t thought of that. I haven’t used VS Code for .NET development, only VS, and never used Rider. Perhaps worth looking into

Still struggling with this since the TUI is based upon a terminal interface and what I’m working on is a windows application. I understand how to get the prompts from the data object of a simple dialog box and can display this just fine. I have yet to understand where in the code it returns a value through the data object. The code below shows the simple messagebox implementation. Any clue on the reflection or dataobject property/view/window/provider that returns the ok/cancel result when doing a messagebox via gui.cs?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace OpenTap.SimpleUI
{
public class RemoteUserInputHandler : IUserInputInterface
{
string getMessage(List members, object dataObject)
{
var message = members.FirstOrDefault(m => m.Get()?.Name?.Equals(“message”, StringComparison.OrdinalIgnoreCase) == true)?.Get()?.Value;
if (message == null)
message = members.FirstOrDefault(m => m.Get()?.Name?.Equals(“name”, StringComparison.OrdinalIgnoreCase) == true)?.Get()?.Value;
if (message == null)
{
var typeData = TypeData.FromType(dataObject.GetType());
message = typeData.Display?.Name ?? typeData.Name;
}

        return message;
    }
    public void RequestUserInput(object dataObject, TimeSpan Timeout, bool modal)
    {
        // Load properties
        var annotations = AnnotationCollection.Annotate(dataObject);
        var members = annotations?.Get<IMembersAnnotation>()?.Members?.ToList();
        // Create dialog

        var dialogMessage = getMessage(members, dataObject);

        var myResult = AnnotationCollection.Annotate(dataObject).Get<IObjectValueAnnotation>();

        MessageBoxResult MyMB =    MessageBox.Show(dialogMessage, "", MessageBoxButton.OKCancel);
        myResult.Value = MyMB;
    }
}

}

The OK/Cancel button part is handled by OpenTAP.TUI/Views/PropertiesView.cs · master · OpenTAP / Plugins / Keysight Technologies / OpenTAP-TUI · GitLab

The idea is that a member has an SubmitAttribute which means that selecting a value for that member also closes the dialog. If a object does not have a member with a SubmitAttribute, a single OK button should be shown. If the member does have a SubmitAttribute, that member should also have a IAvailableValueProxy annotation which can be used to select how many buttons to show in the button of the dialog. Normally though there would only be two available values OK and Cancel, but it could also be e.g Yes/No/Cancel.

Each AnnotationCollection for the buttons will probably have an IReadOnlyStringValueAnnotation which specifies what text to put inside of the button.

submitMember = members.FirstOrDefault(m => m.Get<IAccessAnnotation>().IsVisible && m.Get<IMemberAnnotation>()?.Member.GetAttribute<SubmitAttribute>() != null);

if(submitMember == null) {
   // create an OK button
}else{
   var availableButtons = submitMember.Get<IAvailableValuesAnnotationProxy>().AvailableValues
   // create a button for each AnnotationCollection in availableButtons
}

// before returning from RequestUserInput write the annotation data back to the 'dataObject'
annotations.Write(); 

1 Like

Thank you, I now have a windows MessageBox dialog functional in the example GUI. Next step is to understand how to pass a numeric value back similar to the Serial Number dialog in the example test steps. Any idea which file is best reviewed for this?

The issue I think I have is the get below creates a link to the serialnumber object I want to change when I look at the varEdit variable. However, the varEdit.Value = changes the declaration of the “Source” when I do the annotation. I’m assuming I need to parse the varEdit to get to the storage location for the ulong.

            var varEdit =   annotations.Get<IObjectValueAnnotation>();
            if (varEdit != null) 
            {
                varEdit.Value = (ulong) 12345;
                annotations.Write();                    // Updates Members
            }

I think I finally figured this out using master/Engine/UserInput.cs. While I still don’t understand the relationship between all the parsing involved, I can now return a value to the test step through RequestUserInput…

Here’s a starting point for the next user of the Example GUI. It supports dialog and input boxes, but not timeouts.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.VisualBasic;

namespace OpenTap.Tui
{
public class RemoteUserInputHandler : IUserInputInterface
{
string getTitle(List members, object dataObject)
{
var title = members.FirstOrDefault(m => m.Get()?.Name?.Equals(“title”, StringComparison.OrdinalIgnoreCase) == true)?.Get()?.Value;
if (title == null)
title = members.FirstOrDefault(m => m.Get()?.Name?.Equals(“name”, StringComparison.OrdinalIgnoreCase) == true)?.Get()?.Value;
if (title == null)
{
var typeData = TypeData.FromType(dataObject.GetType());
title = typeData.Display?.Name ?? typeData.Name;
}

        return title;   
    }

    string getMessage(List<AnnotationCollection> members, object dataObject)
    {
        var message = members.FirstOrDefault(m => m.Get<DisplayAttribute>()?.Name?.Equals("message", StringComparison.OrdinalIgnoreCase) == true)?.Get<IStringValueAnnotation>()?.Value;
        if (message == null)
            message = members.FirstOrDefault(m => m.Get<DisplayAttribute>()?.Name?.Equals("name", StringComparison.OrdinalIgnoreCase) == true)?.Get<IStringValueAnnotation>()?.Value;
        if (message == null)
        {
            var typeData = TypeData.FromType(dataObject.GetType());
            message = typeData.Display?.Name ?? typeData.Name;
        }

        return message;
    }
    public void RequestUserInput(object dataObject, TimeSpan Timeout, bool modal)
    {
        // Load properties
        var annotations = AnnotationCollection.Annotate(dataObject);
        var members = annotations?.Get<IMembersAnnotation>()?.Members?.ToList();
        
        var anyAvailableValuesAnnotation = annotations?.Get<INamedMembersAnnotation>();

        var submitMembers = members?.FirstOrDefault(m => m.Get<IAccessAnnotation>().IsVisible && m.Get<IMemberAnnotation>()?.Member.GetAttribute<SubmitAttribute>() != null);
        if (submitMembers != null)
        {
            var myAvailableValuesAnnotation = submitMembers.Get<IAvailableValuesAnnotationProxy>();
            if (myAvailableValuesAnnotation == null)
            {
                /*  ??? TBD ???  */

            }
        }

        // Create dialog
        var dialogTitle = getTitle(members, dataObject);
        var dialogMessage = getMessage(members, dataObject);


        MessageBoxButton myMbButtons = MessageBoxButton.YesNo;
        var submit = members?.FirstOrDefault(m => m.Get<IAccessAnnotation>().IsVisible && m.Get<IMemberAnnotation>()?.Member.GetAttribute<SubmitAttribute>() != null);
        if (submit != null)
        {
            var myAvailableValuesAnnotation = submit.Get<IAvailableValuesAnnotationProxy>();

            foreach (var myAvailableValue in myAvailableValuesAnnotation.AvailableValues)
            {
                var buttonName = myAvailableValue.Source.ToString();
                if (buttonName == "Ok") myMbButtons = MessageBoxButton.OKCancel;
                if (buttonName == "Yes") myMbButtons = MessageBoxButton.YesNo;
            }

            MessageBoxResult MyMB = MessageBox.Show(dialogMessage, dialogTitle, MessageBoxButton.OKCancel);

            if ((MyMB == MessageBoxResult.Cancel) || (MyMB == MessageBoxResult.No))
            {
                foreach (var myAvailableValue in myAvailableValuesAnnotation.AvailableValues)
                {
                    var buttonName = myAvailableValue.Source.ToString();

                    if ((buttonName == "No") || (buttonName == "Cancel"))
                    {
                        var buttonSelectedValue = myAvailableValuesAnnotation.SelectedValue;
                        var buttonAVailableValue = myAvailableValue;
                        myAvailableValuesAnnotation.SelectedValue = myAvailableValue;
                        submitMembers.Write();
                        annotations.Write();
                    }
                }
            }
            if ((MyMB == MessageBoxResult.OK) || (MyMB == MessageBoxResult.Yes))
            {
                foreach (var myAvailableValue in myAvailableValuesAnnotation.AvailableValues)
                {
                    var buttonName = myAvailableValue.Source.ToString();

                    if ((buttonName == "Ok") || (buttonName == "Yes"))
                    {
                        var buttonSelectedValue = myAvailableValuesAnnotation.SelectedValue;
                        var buttonAVailableValue = myAvailableValue;
                        myAvailableValuesAnnotation.SelectedValue = myAvailableValue;
                        submitMembers.Write();
                        annotations.Write();
                    }
                }
            }
        }
        else 
        {
            foreach (var _message in members)
            {
                var mem = _message.Get<IMemberAnnotation>()?.Member;
                if (mem != null)
                {
                    var str = _message.Get<IStringValueAnnotation>();
                    if (str != null)
                    {
                        var name = _message.Get<DisplayAttribute>()?.Name;

                        var isVisible = _message.Get<IAccessAnnotation>()?.IsVisible ?? true;
                        if (!isVisible) continue;

                        var isReadOnly = _message.Get<IAccessAnnotation>()?.IsReadOnly ?? false;
                        if (isReadOnly)
                        {
                            Console.WriteLine($"{str.Value}");
                            continue;
                        }

                        str.Value = Interaction.InputBox(dialogMessage, name, "999999999", -1, -1);
                        _message.Write();
                        annotations.Write();                    // Updates Members
                    }
                }
            }
        }

        /*
        resetEvent.Wait();
        if (timedOut)
            throw new TimeoutException("User input timed out.");
        */
    }
}

}