Problems when re-assigning a PathGeometry in Silverlight

I wasn’t planning in submitting (yet) another workaround for a Silverlight bug, but I stumbled across (yet) another annoying issue that causes no problems in WPF, but results in a exception being thrown in Silverlight. It definitely seems like another Silverlight bug still present in Silverlight 4.

The problem

I was building Paths by re-using a pre-built collection of PathGeometries. However when re-assigning a PathGeometry to another Path like this:

Path path1;
Path path2; // These paths are in the visual tree
// Create a PathGeometry with some points
var points = new PointCollection();
points.Add(new Point(100, 0));
points.Add(new Point(100, 100));
points.Add(new Point(0, 100));
 
var pathFigure = new PathFigure();
pathFigure.StartPoint = new Point(0, 0);
pathFigure.Segments.Add(new PolyLineSegment() { Points = points });
 
var pathGeometry = new PathGeometry();
pathGeometry.Figures.Add(pathFigure);
 
// Assign this PathGeometry to both Paths
path1.Data = pathGeometry;
// No you don't you get an ArgumentException
path2.Data = pathGeometry;

When trying to re-assign this PathGeometry, an ArgumentException was thrown with the message Value does not fall within the expected range. After googling around it turned out others have come across this issue back in mid 2008. This issue is only present in Silverlight, not WPF, which definititely suggests it is a bug.

However, having received a completely meaningless exception, I decided to dig deeper to at least try to understand what went wrong and find a workaround.

The cause of the problem

From the problem it was obvious that there is some problem with assigning the same PathGeometry object to multiple Path objects. Fair enough, if the same object won’t work, how about cloning it? For starters I implemented a shallow clone extension method and tested it out:

// Create a shallow copy method
public static PathGeometry CloneShallow(this PathGeometry pathGeometry)
{
    var newPathGeometry = new PathGeometry();
    foreach (var figure in pathGeometry.Figures)
    {
        newPathGeometry.Figures.Add(figure);
    }
    return newPathGeometry;
}
 
var pathGeometry;
path1.Data = pathGeometry;
// Try assigning the shallow cloned copy of this pathGeometry to the Path
// No you don't, this time an InvalidOperationException is thrown
path2.Data = pathGeometry.CloneShallow();

This didn’t work either, I still got an exception. The exception was at least more helpful this time: it was an InvalidOperationException with the message Element is already the child of another element. This probably means that unlike WPF, the Silverlight engine probably registers PathGeometries and their figures as well in the visual tree, not allowing them to be placed under another sub tree if they are already in the tree themselves.

This just means that I would have to clone even deeper until the point that I get to elements that aren’t registered somewhere (probably in the visual tree) as child elements. This is what I did.

The workaround

The workaround that finally solved the problem was using a deep clone method that cloned all the way down until it got to Point instances which are no longer registered as children of another element (again, probably in the visual tree). This deep clone method looks like the following:

public static PathGeometry CloneDeep(this PathGeometry pathGeometry)
{
    var newPathGeometry = new PathGeometry();
    foreach (var figure in pathGeometry.Figures)
    {
        var newFigure = new PathFigure();
        newFigure.StartPoint = figure.StartPoint;
        // Even figures have to be deep cloned. Assigning them directly will result in
        //  an InvalidOperationException being thrown with the message "Element is already the child of another element."
        foreach (var segment in figure.Segments)
        {
            // I only impemented cloning the abstract PathSegments to one implementation, 
            //  the PolyLineSegment class. If your paths use other kinds of segments, you'll need
            //  to implement that kind of coding yourself.
            var segmentAsPolyLineSegment = segment as PolyLineSegment;
            if (segmentAsPolyLineSegment != null)
            {
                var newSegment = new PolyLineSegment();
                foreach (var point in segmentAsPolyLineSegment.Points)
                {
                    newSegment.Points.Add(point);
                }
                newFigure.Segments.Add(newSegment);
            }                    
        }
        newPathGeometry.Figures.Add(newFigure);
    }
    return newPathGeometry;
}

To understand the problem and the workaround a bit more, here is a sample application that does the three steps: it tries to copy the PathGeometry of the top blue rectangle first by directly assigning, then by shallow cloning and finally by deep cloning:

You can download the source of this application here: PathGeometryProblem.zip