C# WPF ArgumentException: Specified Visual is already a child of another Visual or the root of a CompositionTarget

StackOverflow https://stackoverflow.com/questions/21883586

Frage

I got a list with Images and their Left/Top locations, that I add to a Canvas. However, I want to be able to add the same Images (same source) to the Canvas, without any problems.

When I just use the following code:

Image img = ImagesList[i].Image; // ImagesList is a list of MyClass (containing Image Image; double Left; double Top)
img.Name = "img" + i; // where i is the nr in the list
Canvas.SetLeft(img, ImagesList[i].Left); // Left default = 0
Canvas.SetTop(img, ImagesList[i].Top); // Top default = 0
MyCanvas.Children.Add(img);
OnPropertyChanged("MyCanvas");

when that same Image(-source) is already present on the Canvas (with a different Left/Top location and Name), I get the following exception:

ArgumentException: Specified Visual is already a child of another Visual or the root of a CompositionTarget.

So I know I'm not allowed to add the same UIElement (in my case Image) to the same Canvas.

I modified my code to:

// If the Image already exists on the Canvas, we need to make a clone of the image
if (MyCanvas.Children.Contains(img)) {
    Image cloneImg = new Image();
    cloneImg.Source = img.Source;
    cloneImg.Name = img.Name;
    Canvas.SetLeft(cloneImg, Left);
    Canvas.SetTop(cloneImg, Top);
    MyCanvas.Children.Add(cloneImg);
}
else
    MyCanvas.Children.Add(img);
OnPropertyChanged("MyCanvas");

This resolved the error, but now I have a new problem.. I do get two Images on the Canvas, but the Image already present got it's location (Left and Top) reset to 0,0 (the same as the newly added Image), and when I made a Console.Write-test, I also noticed the Name of both Images on the Canvas are the same now.

What am I doing wrong with the Cloning which makes the first Image's Name, Left, Top (and probably other things) the same as the second Image (the "Clone")?

Thanks in advance for the responses.


EDIT: After the suggestion of Clemens, I changed my xaml from:

<Canvas Name="MyCanvas" Background="LimeGreen"/>

To:

<ItemsControl ItemsSource="{Binding MyField.ImagesList}"> <!-- MyField is the class where I have my list -->
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Name="FieldCanvas" Background="LimeGreen" IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Left" Value="{Binding Left}"/>
                <Setter Property="Canvas.Top" Value="{Binding Top}"/>
            </Style>
        </ItemsControl.ItemContainerStyle>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Image Source="{Binding ImageSource}" AllowDrop="True" PreviewMouseLeftButtonDown="Image_PreviewMouseLeftButtonDown" PreviewMouseMove="Image_PreviewMouseMove" PreviewMouseLeftButtonUp="Image_PreviewMouseLeftButtonUp"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>

The ImagesList now also contains BitmapImage ImagesSource instead of Image Image.

But I wasn't able to test it yet, because it gives a bunch of errors. I use the Canvas as a parameter in the Constructor of the ViewModel for instance, but don't know how to access it now that it's in a ItemsControl. And I also get some errors within my MouseEvent functions, where I use the img.Parent to get the Canvas (which isn't working anymore with the code above) AND canvas.Children to get all the images including their ZIndex (which also isn't working anymore).


EDIT2 / SOLUTION:

After undoing the Edit above because I was getting to much errors because of my other crappy code parts, except for the ImageSource I saved in the list instead of the Image itself, it turns out it works now.

War es hilfreich?

Lösung

You would typically do this with an ItemsControl that has a Canvas as ItemsPanel and an ItemContainerStyle that binds the Canvas.Left and Canvas.Top properties to appropriate properties in your data item class.

If this is your data item class:

public class ImageItem
{
    public string Source { get; set; }
    public double Left { get; set; }
    public double Top { get; set; }
}

public class ViewModel
{
    public ViewModel()
    {
        ImageItems = new ObservableCollection<ImageItem>();
    }

    public ObservableCollection<ImageItem> ImageItems { get; private set; }
}

the XAML of the ItemsControl would look like shown below. Note that its ItemsSource property is bound to the ImageItems property in your ViewModel class.

<ItemsControl ItemsSource="{Binding ImageItems}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas IsItemsHost="True"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Left" Value="{Binding Path=Left}"/>
                <Setter Property="Canvas.Top" Value="{Binding Path=Top}"/>
            </Style>
        </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Image Source="{Binding Path=Source}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Somewhere in MainWindow initialization code:

var vm = new ViewModel();
DataContext = vm;

vm.ImageItems.Add(
    new ImageItem
    {
        Source = @"C:\Users\Public\Pictures\Sample Pictures\Koala.jpg",
        Left = 100,
        Top = 50
    });
vm.ImageItems.Add(
    new ImageItem
    {
        Source = @"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg",
        Left = 200,
        Top = 100
    });

Andere Tipps

After I reverted everything back that I tried with Clemens suggestion, while keeping the change of BitmapImage ImageSource instead of Image Image in my list, it works now.

I know my code isn't the best, and that Clemens code is indeed a lot better. But since I already had quite a lot of code, and it was (in my opinion) too much trouble to change it to Clemens code while remaining working the same with my other code, I reverted it back except the change of the BitmapImage ImageSource, and it turned out it works now.

If I ever do a similar project, I use your code from the start Clemens, since it's much better in terms of WPF programming & Architecture of the code. So thanks for your reply.

Solution for my problem: Instead of saving the Image itself in the list and make a "clone" of it, I just saved my ImageSource in the list instead. So I always make a new Image() from which I change the Source.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top