Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving a Control from one parent to another throws an Exception #7381

Closed
RationalFragile opened this issue Jan 17, 2022 · 2 comments · Fixed by #8427
Closed

Moving a Control from one parent to another throws an Exception #7381

RationalFragile opened this issue Jan 17, 2022 · 2 comments · Fixed by #8427
Labels

Comments

@RationalFragile
Copy link

Describe the bug
If you have one control, you move it from parent1 to parent2, a NullReferenceException is thrown:

Exception details
System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=Avalonia.Visuals
  StackTrace:
   at Avalonia.Rendering.DeferredRenderer.Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds) in C:\Avalonia\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 433

  This exception was originally thrown at this call stack:
    Avalonia.Rendering.DeferredRenderer.Render(Avalonia.Platform.IDrawingContextImpl, Avalonia.Rendering.SceneGraph.VisualNode, Avalonia.VisualTree.IVisual, Avalonia.Rect) in DeferredRenderer.cs

To Reproduce

<DockPanel>
    <Button Command="{Binding Move}">Move</Button>
    <Grid Name="Parent">
        <TextBlock>content</TextBlock>
    </Grid>
    <Grid Name="Parent2">

    </Grid>
</DockPanel>

And

public void Move(){
  var parent = this.FindControl<Grid>("Parent");
  var parent2 = this.FindControl<Grid>("Parent2");
  if (parent.Children.Count > 0)
  {
      System.Console.WriteLine("1 -> 2");
      var child = parent.Children[0];
      parent.Children.Clear();
      parent2.Children.Add(child);
  }
  else
  {
      System.Console.WriteLine("1 <- 2");
      var child = parent2.Children[0];
      parent2.Children.Clear();
      parent.Children.Add(child);
  }
}

Expected behavior
TextBlock should be removed from the old parent and added to the new parent.

Desktop (please complete the following information):

  • OS: Windows 10 (20H2 19042.868)
  • Version 0.10.11

Additional context
It doesn't work with other types of parents as well (e.g. Border).
Also, the Exception is thrown here:

private void Avalonia.Visuals.Rendering.DeferredRenderer.Render
(IDrawingContextImpl context, VisualNode node, IVisual? layer, Rect clipBounds)
        {
[...]
                    for (int i = 0; i < drawOperationsCount; i++)
                    {
                        var operation = drawOperations[i];
                        _currentDraw = operation;
                        operation.Item.Render(context); // << Item is null
                        _currentDraw = null;
                    }
[...]
}

Simply adding a null check at operation.Item?.Render(context); will cause other Exceptions:

Exception details
System.ObjectDisposedException
  HResult=0x80131622
  Message=Cannot access a disposed object.
Object name: 'Ref<System.IDisposable>'.
  Source=Avalonia.Base
  StackTrace:
   at Avalonia.Utilities.RefCountable.Ref`1.Clone() in C:\Avalonia\src\Avalonia.Base\Utilities\Ref.cs:line 186
   at Avalonia.Rendering.SceneGraph.VisualNode.Clone(IVisualNode parent) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\VisualNode.cs:line 262
   at Avalonia.Rendering.SceneGraph.Scene.Clone(VisualNode source, IVisualNode parent, Dictionary`2 index) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 165
   at Avalonia.Rendering.SceneGraph.Scene.Clone(VisualNode source, IVisualNode parent, Dictionary`2 index) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 179
   at Avalonia.Rendering.SceneGraph.Scene.Clone(VisualNode source, IVisualNode parent, Dictionary`2 index) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 179
   at Avalonia.Rendering.SceneGraph.Scene.Clone(VisualNode source, IVisualNode parent, Dictionary`2 index) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 179
   at Avalonia.Rendering.SceneGraph.Scene.Clone(VisualNode source, IVisualNode parent, Dictionary`2 index) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 179
   at Avalonia.Rendering.SceneGraph.Scene.Clone(VisualNode source, IVisualNode parent, Dictionary`2 index) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 179
   at Avalonia.Rendering.SceneGraph.Scene.Clone(VisualNode source, IVisualNode parent, Dictionary`2 index) in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 179
   at Avalonia.Rendering.SceneGraph.Scene.CloneScene() in C:\Avalonia\src\Avalonia.Visuals\Rendering\SceneGraph\Scene.cs:line 91
   at Avalonia.Rendering.DeferredRenderer.UpdateScene() in C:\Avalonia\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 635
   at Avalonia.Rendering.DeferredRenderer.UpdateSceneIfNeeded() in C:\Avalonia\src\Avalonia.Visuals\Rendering\DeferredRenderer.cs:line 619
   at Avalonia.Threading.JobRunner.Job.Avalonia.Threading.JobRunner.IJob.Run() in C:\Avalonia\src\Avalonia.Base\Threading\JobRunner.cs:line 177
   at Avalonia.Threading.JobRunner.RunJobs(Nullable`1 priority) in C:\Avalonia\src\Avalonia.Base\Threading\JobRunner.cs:line 37
   at Avalonia.Win32.Win32Platform.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam) in C:\Avalonia\src\Windows\Avalonia.Win32\Win32Platform.cs:line 263
   at Avalonia.Win32.Interop.UnmanagedMethods.DispatchMessage(MSG& lpmsg)
   at Avalonia.Win32.Win32Platform.RunLoop(CancellationToken cancellationToken) in C:\Avalonia\src\Windows\Avalonia.Win32\Win32Platform.cs:line 210
   at Avalonia.Threading.Dispatcher.MainLoop(CancellationToken cancellationToken) in C:\Avalonia\src\Avalonia.Base\Threading\Dispatcher.cs:line 61
   at Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime.Start(String[] args) in C:\Avalonia\src\Avalonia.Controls\ApplicationLifetimes\ClassicDesktopStyleApplicationLifetime.cs:line 132
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime[T](T builder, String[] args, ShutdownMode shutdownMode) in C:\Avalonia\src\Avalonia.Controls\ApplicationLifetimes\ClassicDesktopStyleApplicationLifetime.cs:line 187
   at Sandbox.Program.Main(String[] args) in C:\Avalonia\samples\Sandbox\Program.cs:line 7

@RationalFragile
Copy link
Author

The issue only happens when you swap the children of two parents and one of them has no children.

Here is an updated test code:

<DockPanel>
  <Button Command="{Binding $parent[Window].Move}">Move</Button>
  <Grid Name="Parent">
    <TextBlock>content</TextBlock>
  </Grid>
  <Grid Name="Parent2">
<!-- if you uncomment this child, the code will execute without exceptions-->
    <!-- <TextBlock>content2</TextBlock> -->
  </Grid>
</DockPanel>
public void Move() {
  var parent = this.FindControl<Grid>("Parent");
  var parent2 = this.FindControl<Grid>("Parent2");
  List<IControl> c1 = new();
  foreach (var c in parent.Children) {
      c1.Add(c);
  }

  List<IControl> c2 = new();
  foreach (var c in parent2.Children) {
      c2.Add(c);
  }
  parent.Children.Clear();
  parent2.Children.Clear();

  parent.Children.AddRange(c2);
  parent2.Children.AddRange(c1);
}

(Also, same behavior for Border.Child, if one of the parents has no child, it will throw...)
The issue still exists in 0.10.12

@RationalFragile
Copy link
Author

Following my last comment, a temporary workaround is to simply check if the parent has no children/child and assign to it an empty control before assigning the swapped child. (And maybe delete it later depending on your logic...) Like this:

            var parent = this.FindControl<Grid>("Parent");
            var parent2 = this.FindControl<Grid>("Parent2");

           // avoiding assigning a swapped child to a parent that doesn't have children
            if (parent.Children.Count == 0) {
                parent.Children.Add(new Control());
            }
            if (parent2.Children.Count == 0) {
                parent2.Children.Add(new Control());
            }

            List<IControl> c1 = new();
            foreach (var c in parent.Children) {
                c1.Add(c);
            }

            List<IControl> c2 = new();
            foreach (var c in parent2.Children) {
                c2.Add(c);
            }
            parent.Children.Clear();
            parent2.Children.Clear();

            parent.Children.AddRange(c2);
            parent2.Children.AddRange(c1);

grokys added a commit that referenced this issue Jul 1, 2022
grokys added a commit that referenced this issue Jul 1, 2022
When a control was move from one parent container to another, and that move caused the new parent container to be laid out in a different position, a code path was taken which resulted in the `VisualNode` being present under both the old and new containers.

Ensure that the node is removed from its old parent in this case.

Fixes #7381
Fixes #6103 (probably)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant