ITapDockPanel to display information from an executing TestStep

I would like to create an ITapDockPanel that can be updated from a TestStep in a running test plan without using Results.Publish .
I’ve tried using the TestStep as the ViewModel for the DataContext of the WPF UserControl, but my WPF is limited and I suspect I’m trying to do something that isn’t possible.

The DockablePanel example works great using the OnResultPublished event of the ResultlLstener, but I don’t want to use it because I’ll just end up spamming the database with unwanted results. I would also like to be able to have the information flow bi-directionally, hence the ViewModel approach.

Has anyone else been able to do something similar?

Hi Jason,

The DockablePanel example demonstrates how to use a result listener to get information about the currently executing test plan. Result listeners were never intended as a generic means of transferring data within your application, so I am happy that you are not using it this way. That would indeed be a lot of garbage in your result database!

There are many ways you could achieve a bi-directional flow of information between a dock panel and a test step. The most straight forward way I can think of would be to leverage the ITapDockContext you are provided with as part of the ITapDockPanel interface:

    public interface ITapDockPanel : ITapPlugin
    {
        /// <summary> Creates an instance of the custom GUI Panel. </summary>
        /// <param name="context"> The context object. </param>
        /// <returns>The framework element that is shown in the new panel.</returns>
        FrameworkElement CreateElement(ITapDockContext context);
       // etc
    }

If you hold on to the ITapDockContext, you can use it to retrieve the list of test steps in the currently loaded test plan. Here is a simple example of updating your UI in response to something happening in a test step:

var plan = context.Plan;
var myStep = context.Plan.ChildTestSteps.RecursivelyGetAllTestSteps(TestStepSearch.EnabledOnly)
    .OfType<DelayStep>().FirstOrDefault();
myStep.PropertyChanged += (sender, eventArgs) =>
{
    if (eventArgs.PropertyName == nameof(DelayStep.DelaySecs))
    {
        // do something in response to myStep.DelaySecs changing
        // In WPF, this typically needs to happen in the GUI thread
        GuiHelpers.GuiInvoke(() =>
        {
            this.CurrentDelaySecs = myStep.DelaySecs;
        });
    }
}; 

Since you have a direct reference to the test steps in your test plan, you will be able to achieve bi-directional data flow.

I hope this is helpful!

Hi Alexander,
Thanks for the quick reply. I’ve looked at it, but I’m a bit confused as to how can update the dock panel from the test step. I’m trying to modify the example dock panel, but not having too much luck. Are you able to provide a complete example?

The top DockPanel uses the ResultListener and the lower DockPanel uses your approach. You can see the panel is not getting the count updated.

Br
Jase

Hi Jason,

Sorry for the late reply. It has been a hectic week. I don’t know if you were able to figure it out on your own, but I have created a more complete example that illustrates how to handle updates when e.g. a new step is added to the test plan, or the test plan is reloaded. It also shows how to achieve GUI updates based on events, in addition to updating the GUI directly from the step.

Note that I don’t recommend creating such a strong coupling between your GUI and test steps.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Xml.Serialization;
using Keysight.OpenTap.Gui;
using Keysight.OpenTap.Wpf;
using OpenTap;

namespace WpfExample
{
    public class MyTestStep : TestStep
    {
        [XmlIgnore]
        internal MyDockPanel dockPanel { get; set; }

        private string _textBlockText;
        
        [XmlIgnore]
        public string TextBlockText
        {
            get => _textBlockText;
            set
            {
                if (value == _textBlockText) return;
                _textBlockText = value;
                OnPropertyChanged(nameof(TextBlockText));
            }
        }

        public List<string> AlternatingStrings { get; set; } = new List<string>();
        public override void Run()
        {
            foreach (var s in AlternatingStrings)
            {
                TextBlockText = s;
                if (dockPanel?.FirstTextBlock is TextBlock t)
                    GuiHelpers.GuiInvoke(() =>
                    {
                        t.Text = new string(s.Reverse().ToArray());
                    });
                TapThread.Sleep(TimeSpan.FromSeconds(2));
                Log.Info($"Set text to {s}");
            }
        }
    }
    
    public class MyDockPanel: ITapDockPanel
    {
        [Browsable(false)]
        class dockResultListener : ResultListener
        {
            public Action RunStarted = null;
            public override void OnTestPlanRunStart(TestPlanRun planRun)
            {
                RunStarted?.Invoke();
            }
        }
        
        public TextBlock FirstTextBlock { get; set; }
        public TextBlock SecondTextBlock { get; set; }

        private TestPlan previousTestPlan = null;
        void AddPlanHooks(TestPlan plan)
        {
            if (plan == previousTestPlan || plan == null) return;
            previousTestPlan = plan;
            var steps = plan.ChildTestSteps.RecursivelyGetAllTestSteps(TestStepSearch.EnabledOnly)
                .OfType<MyTestStep>().ToArray();

            void addStepHooks(MyTestStep step)
            {
                step.dockPanel = this;
                step.PropertyChanged += (sender, args) =>
                {
                    if (args.PropertyName == nameof(MyTestStep.TextBlockText))
                        GuiHelpers.GuiInvoke(() => SecondTextBlock.Text = step.TextBlockText);
                };
            }
            
            foreach (var step in steps)
            {
                addStepHooks(step);
            }

            plan.ChildTestSteps.ChildStepsChanged += (list, action, step, index) =>
            {
                if (action == TestStepList.ChildStepsChangedAction.AddedStep && step is MyTestStep s)
                    addStepHooks(s);
            };
        }

        public FrameworkElement CreateElement(ITapDockContext context)
        {
            var rl = new dockResultListener();
            context.ResultListeners.Add(rl);
            rl.RunStarted = () =>
            {
                GuiHelpers.GuiInvoke(() => AddPlanHooks(context.Plan));
            };
            AddPlanHooks(context.Plan);
            var panel = new StackPanel() { Orientation = Orientation.Vertical };

            FirstTextBlock = new TextBlock()
            {
                FontSize = 40,
                HorizontalAlignment = HorizontalAlignment.Center,
                Text = "Nothing here yet."
            };
            
            SecondTextBlock = new TextBlock()
            {
                FontSize = 40,
                HorizontalAlignment = HorizontalAlignment.Center,
                Text = "Nothing here yet."
            };
            
            panel.Children.Add(FirstTextBlock);
            panel.Children.Add(SecondTextBlock);

            return panel;
        }

        public double? DesiredWidth => 200;
        public double? DesiredHeight => 200;
    }
}

Animation

2 Likes

Cheers Alexander.
This is a really elegant solution, and the direct and indirect approaches clearly demonstrate its usefulness.
My way was a bit more convoluted than this, but its performance was very similar.

Thanks again.

1 Like