Custom node UI not updating whilst node is running

I’m creating a custom node that runs a process when a command button is pressed, and whilst the process is running I want it to update a simple line graph (just a polyline drawn on a canvas using WPF). The node runs at the moment and completes the process, but the UI is only updated when the process is complete, not after each step of the loop. I want the UI to update during the process as it can take a few minutes to complete and I want to provide visual feedback during that time. The code that’s run on the button press is shown below in abbreviated form, and is located in the NodeModel class:

    private void buttonCommand(object obj)
    {
        System.Windows.Media.PointCollection newPoints = new System.Windows.Media.PointCollection();
        for (int i = 0; i < testInt; i++)
        {
            //iterative process sits here
            //new points collection is generated and stored in newPoints
            //MyPoints set to newPoints. MyPoints is bound to a polyline in the UI
            MyPoints = newPoints;
        }
        OnNodeModified();
    }

I’ve also tried updating the MyPoints property on the UI thread:
private void buttonCommand(object obj)
{
System.Windows.Media.PointCollection newPoints = new System.Windows.Media.PointCollection();
for (int i = 0; i < testInt; i++)
{
//iterative process sits here
//new points collection is generated and stored in newPoints
//MyPoints set to newPoints. MyPoints is bound to a polyline in the UI
DispatchOnUIThread(delegate
{
MyPoints = newPoints;
});
}
OnNodeModified();
}
but this doesn’t work either.

In both cases I know the iterative process and the points collection is working because once it is complete, the node UI updates correctly.

If it helps, the below is an image of the node. I basically want the polyline to be updated in the view at each step of the iteration, not just once it is complete:

I’m not sure why this isn’t working, I think we’ll need a full working repo to help out. Is this in Revit or Studio?

They have different threading contexts which may confuse the issue further.

just some basic questions though - are you sure that your property reassignment is to myPoints is raising the property notification that you are bound to? - have you tried debugging this node yet?

last idea without being able to see more code: try setting the priority of your callback on the dispatcher higher.

Hi Michael
Thanks for taking a look. I think I’m blocking the UI thread with my loop but I haven’t figured out a way to solve this yet, so any suggestions you have would be great. The full code is below (I’ve simplified the loop for bug testing - it should just draw a moving line for a period of 10 seconds). At the moment the loop runs when the ‘go’ button is pressed but just displays the final position of the line, it does not update it whilst the loop is running.

[NodeName(“Iteration”)]
[NodeCategory(“Analysis tools”)]
[NodeDescription(“NodeDescription”, typeof(ClassLibrary5.Properties.Resources))]
[IsDesignScriptCompatible]

public class CustomNodeModel4 : NodeModel
{
    public event EventHandler RequestChange;
    protected virtual void OnRequestChange(object sender, EventArgs e)
    {
        if (RequestChange != null)
            RequestChange(sender, e);
    }

    private System.Windows.Media.PointCollection myPoints = new System.Windows.Media.PointCollection();
    public System.Windows.Media.PointCollection MyPoints
    {
        get { return myPoints; }
        set
        {
            myPoints = value;
            RaisePropertyChanged("MyPoints");
        }
    }

    private int testInt = 1;
    public int TestInt
    {
        get { return testInt; }
        set
        {
            testInt = value;
        }
    }

    DynamoModel dynamoModel;

    public DelegateCommand OnButtonCommand { get; set; }

    public CustomNodeModel4()
    {
        InPortData.Add(new PortData("Panel number", Resources.CustomNodeModePortDataInputToolTip));
        OutPortData.Add(new PortData("Empty", Resources.CustomNodeModePortDataOutputToolTip));
        RegisterAllPorts();
        ArgumentLacing = LacingStrategy.CrossProduct;
        OnButtonCommand = new DelegateCommand(buttonCommand);
    }

    void buttonCommand(object obj)
    {
       
        //I've tried running this on a new thread and using a dispatcher to update MyPoints but still not updating during the loop
        //Task.Factory.StartNew(() =>
        //{
            System.DateTime tempTime;
            System.DateTime tempTime2;
            System.Windows.Media.PointCollection tempPoints = new System.Windows.Media.PointCollection();
            for (int i = 0; i < 1000; i++)
            {
                tempPoints.Clear();
                tempPoints.Add(new System.Windows.Point(0, 0));
                tempPoints.Add(new System.Windows.Point(200, (int)(i/10)));
                MyPoints = tempPoints; 
                //Dynamo.Models.DynamoModel.OnRequestDispatcherBeginInvoke(new Action(()=> MyPoints = tempPoints ));
                tempTime = System.DateTime.Now;
                tempTime2 = System.DateTime.Now;
                while (System.DateTime.Now.Subtract(tempTime).TotalMilliseconds < 5)
                {
                }
            }
            MessageBox.Show("task factory");
        //});

               }

    [IsVisibleInDynamoLibrary(false)]
    public override IEnumerable<AssociativeNode> BuildOutputAst(List<AssociativeNode> inputAstNodes)
    {
        return new[]
            {
            AstFactory.BuildAssignment(
                GetAstIdentifierForOutputIndex(0), 
                AstFactory.BuildStringNode(testInt.ToString()))
            };
    }
    
    public class CustomNodeModelNodeViewCustomization : INodeViewCustomization<CustomNodeModel4>
    {
        System.Windows.Shapes.Polyline myPolyline = new Polyline();
        private DynamoModel dynamoModel;

        public void CustomizeView(CustomNodeModel4 model, NodeView nodeView)
        {
            dynamoModel = nodeView.ViewModel.DynamoViewModel.Model;

            var helloDynamoControl = new UserControl5();
            nodeView.inputGrid.Children.Add(helloDynamoControl);
            helloDynamoControl.DataContext = model;

            myPolyline.Stroke = System.Windows.Media.Brushes.DarkCyan;
            myPolyline.HorizontalAlignment = HorizontalAlignment.Left;
            myPolyline.VerticalAlignment = VerticalAlignment.Top;
            myPolyline.StrokeThickness = 3;

            System.Windows.Data.Binding myBinding = new System.Windows.Data.Binding();
            myBinding.Source = model;
            myBinding.Path = new PropertyPath("MyPoints");
            myBinding.Mode = System.Windows.Data.BindingMode.OneWay;
            myBinding.UpdateSourceTrigger = System.Windows.Data.UpdateSourceTrigger.PropertyChanged;
            System.Windows.Data.BindingOperations.SetBinding(myPolyline, Polyline.PointsProperty, myBinding);
            model.MyPoints.Add(new System.Windows.Point(0, 0));
            model.MyPoints.Add(new System.Windows.Point(270, 0));
            helloDynamoControl.graphCanvas.Children.Add(myPolyline);
            model.dynamoModel = nodeView.ViewModel.DynamoViewModel.Model;
        }
        public void Dispose() { }
    }
}

}

Meant to say, I’m doing this in Visual Studio.

but what context are you running in as well, Revit or Dynamo Studio?

but a bit of research suggests that you are blocking the UI thread, if I get a chance will mess about a bit.

see this:

I’m running in Dynamo 0.8.0, running either in Revit or standalone.

I’ve tried Task.Factory.StartNew to run this loop and a dispatcher to update MyPoints on the UI thread, but none seem to work.
The new thread runs OK, but when I try to update MyPoints with a dispatcher it throws an exception. If a just call a messagebox with the dispatcher it runs OK, so it seems to be a problem with updating properties on the model class.
I’ve tried a few different dispatchers, all with the same result:

                DispatchOnUIThread(() => MyPoints = tempPoints);
                Dispatcher.CurrentDispatcher.Invoke(new Action(() => { MyPoints = tempPoints; }));
                Application.Current.Dispatcher.InvokeAsync(new Action(() => { MyPoints = tempPoints ;}));
                Dynamo.Models.DynamoModel.OnRequestDispatcherBeginInvoke(new Action(()=> MyPoints = tempPoints ));

If I just update MyPoints from within the new thread, then the code runs without exception but the graphical display doesn’t update until the loop completes.

Strangely, calling Messagebox.Show appears to force a refresh of the node UI. The method below updates the line position in the node UI after each loop:

    void buttonCommand(object obj)
    {
        for (int i = 0; i < 3; i++)
        {
            drawLineFromOrigin(200, i * 100);
        }
    }
    public void drawLineFromOrigin(int xCoord, int yCoord)
    {
        System.Windows.Media.PointCollection tempPoints = new System.Windows.Media.PointCollection();
        System.DateTime tempTime = new System.DateTime();
        tempPoints.Clear();
        tempPoints.Add(new System.Windows.Point(0, 0));
        tempPoints.Add(new System.Windows.Point(xCoord, yCoord));
        MyPoints = tempPoints;
        MessageBox.Show("pause");
        tempTime = System.DateTime.Now;
        while (System.DateTime.Now.Subtract(tempTime).TotalMilliseconds < 500){}
    }

However, removing the Messagebox means that the node UI is only refreshed once the whole buttonCommand is complete (i.e. it updates once to show the final position, not the intermediate steps). I can’t find the equivalent command in the Dynamo API or elsewhere that would force the node UI to update - any ideas anyone?

Hi Albal: Did you solve this problem?
I am working on the issue now.

No, I didn’t solve this. I got distracted by a new project and haven’t returned to this yet!

It seems that this problem can be solved by BackgroundWorker here http://dotnetpattern.com/wpf-backgroundworker

A Simple fix to the problem is to Start and Commit the Transaction in the For Loop. I was also stuck in the same problem and this fix worked for me. Here is my code example in Python:

for e in obj:
	try:
		elm = app.Selection.PickObject(Selection.ObjectType.Element)
		trans.Start("Recolor Parts")
		View.SetElementOverrides(cur, elm.ElementId, ogs)
		trans.Commit()
	except:
		pass

Before, the Active View was not updating automatically as I selected each element, but with this simple workaround, it worked!