Tuesday, June 29, 2010

Cloning Path Geometry in Silverlight

image

To add depth to a line rendered on a map, I experimented with the drop shadow bitmap effect. The shadow looked great but it had a heavy performance cost. As a workaround, I substituted the bitmap effect with a cloned path. Unfortunately Silverlight does not support path geometry cloning, unlike WPF, so based on a Silverlight.net forums thread I implemented a value converter that clones path geometry.

The second path below is the original street route displayed in blue above. The first path clones the blue path’s geometry and offsets it by a few pixels.

<Path Opacity="0.5"
      StrokeThickness="5"
      StrokeLineJoin="Round"
      Stroke="Gray"
      Data="{Binding ElementName=Element,
                     Path=Data,
                     Mode=OneWay,
                     Converter={StaticResource GeometryConverter}}">
    <Path.Resources>
        <local:GeometryConverter x:Key="GeometryConverter"/>
    </Path.Resources>
    <Path.RenderTransform>
        <TranslateTransform X="6" Y="6" />
    </Path.RenderTransform>
</Path>
<Path x:Name="Element"
StrokeThickness="5"
StrokeLineJoin="Round"
Stroke="Blue"/>

Below is the code for the value converter.  The code uses reflection to clone the geometry of the original path.

public class GeometryConverter : IValueConverter {
    public object Convert(object value, Type targetType,
object parameter, CultureInfo culture) { // Test for Null if (value == null) { return null; } Geometry g = value as Geometry; if (g == null) { return null; } Geometry clone = Clone(g) as Geometry; return clone; } public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture) { throw new NotImplementedException(); } private static object Clone(Object obj) { // Get all properties and clone them. PropertyInfo[] properties = obj.GetType().GetProperties(); object cloneObj = obj.GetType().GetConstructors()[0].Invoke(null); foreach (PropertyInfo property in properties) { object value = property.GetValue(obj, null); if (value != null) { if (IsPresentationFrameworkCollection(value.GetType())) { object collection = property.GetValue(obj, null); int count = (int)collection.GetType().
GetProperty("Count").GetValue(collection, null); for (int i = 0; i < count; i++) { // Get each child of the collection. object child = collection.GetType().
GetProperty("Item").
GetValue(collection, new Object[] { i }); object cloneChild = Clone(child); object cloneCollection = property.
GetValue(cloneObj, null); collection.GetType().
InvokeMember("Add",
BindingFlags.InvokeMethod,
null,
cloneCollection,
new object[] { cloneChild }); } } // If the property is a UIElement, we also need to clone it. else if (value is UIElement) { object obj2 = property.PropertyType.
GetConstructors()[0].Invoke(null); Clone(obj2); property.SetValue(cloneObj, obj2, null); } // For a normal property, its value doesn't need to be
// cloned.
So just copy its value to the new object. else if (property.CanWrite) { property.SetValue(cloneObj, value, null); } } } return cloneObj; } private static bool IsPresentationFrameworkCollection(Type type) { if (type == typeof(object)) { return false; } if (type.Name.StartsWith("PresentationFrameworkCollection")) { return true; } return IsPresentationFrameworkCollection(type.BaseType); } }