Creating Shapes with PathBuilder

In a previous post I showed a quick demo of a PathBuilder class I created to help draw dynamic shapes. I would like to include a simple example of how to create Shape controls using the same tool.

I have created a very simple Triangle control which you can download and see run.

Download Triangle Demo

Here is the actual Triangle class

class Triangle : Shape
{
    private Geometry geometry;

    public static readonly DependencyProperty P1Property = DependencyProperty.Register(
        "P1",
        typeof(Point),
        typeof(Triangle),
        new UIPropertyMetadata(OnPointChanged));

    public static readonly DependencyProperty P2Property = DependencyProperty.Register(
        "P2",
        typeof(Point),
        typeof(Triangle),
        new UIPropertyMetadata(OnPointChanged));

    public static readonly DependencyProperty P3Property = DependencyProperty.Register(
        "P3",
        typeof(Point),
        typeof(Triangle),
        new UIPropertyMetadata(OnPointChanged));

    public Point P1
    {
        get { return (Point)GetValue(P1Property); }
        set { SetValue(P1Property, value); }
    }

    public Point P2
    {
        get { return (Point)GetValue(P2Property); }
        set { SetValue(P2Property, value); }
    }

    public Point P3
    {
        get { return (Point)GetValue(P3Property); }
        set { SetValue(P3Property, value); }
    }

    private static void OnPointChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        ((Triangle)sender).UpdateGeometry();
    }

    private void UpdateGeometry()
    {
        this.geometry = PathBuilder.Start()
            .Move(P1)
            .DrawLine(P2)
            .DrawLine(P3)
            .DrawLine(P1)
            .Close()
            .ToGeometry();
    }

    public Triangle()
    {
        this.UpdateGeometry();
    }
        

    protected override Geometry DefiningGeometry
    {
        get 
        {
            return this.geometry;
        }
    }
}

Notice I inherited from Shape and use the PathBuilder to generate a Geometry object and return it in the protected DefiningGeometry property.

I had to add one method to my PathBuilder:

public static Geometry ToGeometry(this IPath path)
{
    var converter = new GeometryConverter();
    return (Geometry)converter.ConvertFromString(path.Data);
}

This method converts the path string into a Geometry object. Here is the xaml to display this control:

<c:Triangle P1="0,0" P2="0,100" P3="100,100" StrokeThickness="1" Stroke="Black" Fill="Blue" />

And finally the Triangle in action

image

WPF or Silverlight PathBuilder

Every once in a while you find yourself needing to draw some type of a shape dynamically. One easy example is arrows connecting visuals that are movable. In some cases you can simply place arrows where you expect the visuals to be but in others the visuals can move around in non-fixed ways. For those scenarios you need to create lines dynamically.

path_builder_example

In the composite screenshots above I am showing a scenario where I am able to drag the nodes around free form and I need the arrows to continuously update as the visual is moving to show relationships. You can imagine various other scenarios where you would need data driven shapes.

I have created a simple API for creating Path data in WPF or Silverlight. It is based on the idea that you can bind a string to the Data property of a Path object and all you need to do is to produce valid Path Data syntax in a string and you get a dynamic shape.

public enum SweepDirection
{
    Clockwise,
    CounterClockwise
}

public interface IPath
{
    string Data { get;}
}

public static class PathBuilder 
{
    private enum DrawCommand
    {
        Start,
        Move,
        Line,
        HorizontalLine,
        VerticalLine,
        CubicBezierCurve,
        QuadraticBezierCurve,
        SmoothCubicBezierCurve,
        SmoothQuadraticBezierCurve,
        EllipticalArc,
        Close
    }

    public static IPath Start()
    {
        return new Path(DrawCommand.Start, "");
    }

    private class Path : IPath
    {
        private DrawCommand lastCommand = DrawCommand.Start;
        private string data;
        public Path(DrawCommand command, string data)
        {
            this.lastCommand = command;
            this.data = data;
        }

        public string Data
        {
            get { return this.data; }
        }

        public DrawCommand Command
        {
            get { return this.lastCommand; }
        }
    }

    public static IPath Move(this IPath path, Point p)
    {
        var prefix = PathBuilder.AppendCommandPrefix(((Path)path).Command, DrawCommand.Move);
        return new Path(
            DrawCommand.Move, 
            path.Data + prefix + string.Format(" {0} {1}", p.X, p.Y));
    }

    public static IPath DrawLine(this IPath path, Point end)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.Line);
        return new Path(
            DrawCommand.Line, 
            path.Data + prefix + string.Format(" {0} {1}", end.X, end.Y));
    }

    public static IPath DrawHorizontalLine(this IPath path, double x)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.HorizontalLine);
        return new Path(
            DrawCommand.HorizontalLine,
            path.Data + prefix + string.Format(" {0}", x));
    }

    public static IPath DrawVerticalLine(this IPath path, double y)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.VerticalLine);
        return new Path(
            DrawCommand.VerticalLine,
            path.Data + prefix + string.Format(" {0}", y));
    }

    public static IPath DrawCubicBezierCurve(this IPath path, Point controlPoint1, Point controlPoint2, Point end)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.CubicBezierCurve);
        return new Path(
            DrawCommand.CubicBezierCurve,
            path.Data + prefix + string.Format(" {0} {1} {2} {3} {4} {5}",
                controlPoint1.X,
                controlPoint1.Y,
                controlPoint2.X,
                controlPoint2.Y,
                end.X,
                end.Y));
    }

    public static IPath DrawQuadraticBezierCurve(this IPath path, Point controlPoint, Point end)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.QuadraticBezierCurve);
        return new Path(
            DrawCommand.QuadraticBezierCurve,
            path.Data + prefix + string.Format(" {0} {1} {2} {3}",
                controlPoint.X,
                controlPoint.Y,
                end.X,
                end.Y));
    }

    public static IPath DrawSmoothCubicBezierCurve(this IPath path, Point controlPoint, Point end)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.SmoothCubicBezierCurve);
        return new Path(DrawCommand.SmoothCubicBezierCurve,
            path.Data + prefix + string.Format(" {0} {1} {2} {3}",
                controlPoint.X,
                controlPoint.Y,
                end.X,
                end.Y));
    }

    public static IPath DrawSmoothQuadraticBezierCurve(this IPath path, Point controlPoint, Point end)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.SmoothQuadraticBezierCurve);
        return new Path(
            DrawCommand.SmoothQuadraticBezierCurve,
            path.Data + prefix + string.Format(" {0} {1} {2} {3}",
                controlPoint.X,
                controlPoint.Y,
                end.X,
                end.Y));
    }

    public static IPath DrawEllipticalArc(this IPath path, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, Point end)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.EllipticalArc);
        return new Path(
            DrawCommand.EllipticalArc,
            path.Data + prefix + string.Format(" {0} {1} {2} {3} {4} {5} {6}",
                size.Width,
                size.Height,
                rotationAngle,
                isLargeArc ? 1 : 0,
                sweepDirection == SweepDirection.Clockwise ? 0 : 1,
                end.X,
                end.Y));
    }

    public static IPath Close(this IPath path)
    {
        var prefix = AppendCommandPrefix(((Path)path).Command, DrawCommand.Close);
        return new Path(DrawCommand.Close, path.Data + prefix);
    }

    private static string AppendCommandPrefix(DrawCommand last, DrawCommand command)
    {
        if (last != command)
        {
            char c;
            switch (command)
            {
                case DrawCommand.Move:
                    if (last == DrawCommand.Move)
                        throw new InvalidOperationException("Cannot have two move commands in a row.");

                    c = 'M';
                    break;
                case DrawCommand.Line:
                    c = 'L';
                    break;
                case DrawCommand.HorizontalLine:
                    c = 'H';
                    break;
                case DrawCommand.VerticalLine:
                    c = 'V';
                    break;
                case DrawCommand.CubicBezierCurve:
                    c = 'C';
                    break;
                case DrawCommand.QuadraticBezierCurve:
                    c = 'Q';
                    break;
                case DrawCommand.SmoothCubicBezierCurve:
                    c = 'S';
                    break;
                case DrawCommand.SmoothQuadraticBezierCurve:
                    c = 'T';
                    break;
                case DrawCommand.EllipticalArc:
                    c = 'A';
                    break;
                case DrawCommand.Close:
                    if (last == DrawCommand.Close)
                        throw new InvalidOperationException("Cannot have two Close commands in a row.");

                    c = 'Z';
                    break;
                default:
                    throw new NotSupportedException();
            }

            return string.Format(CultureInfo.InvariantCulture, " {0}", c);
        }

        return string.Empty;
    }
}

 

Here is a snippet for drawing the arrows like I am doing above:

var pathData = PathBuilder.Start()
    .Move(start)
    .DrawCubicBezierCurve(cp1, cp2, end)
    .Move(end)
    .DrawLine(ap1)
    .Move(end)
    .DrawLine(ap2)
    .Data;

It’s a fluent interface that returns an immutable IPath for each draw call so you can reuse parts of paths and branch shapes without having to redraw the entire thing every time. I will leave it up to you to figure out where to put all of your points but the above snippet draws an arrow.

I am doing this code in my ViewModel in a string Property based on the state of my model and I am actually rendering it by Binding that to the Data property on a Path object. The related XAML snippet looks like this:

<Path 
    Data="{Binding PathData}" 
    Stroke="Black" 
    StrokeThickness="1" />

As strange as it seems the Data property can accept a string and when bound will redraw the path as the bound property changes.

UnitDriven for Windows Phone 7

I have extended UnitDriven to provide support for (the current beta release of) Windows Phone 7. This means you can write a unit test that runs on .NET, Silverlight and Windows Phone. This is an Alpha release for now, hopefully if we get a few people to use it I can make a more stable release when the final version of the  WindowsPhone7 SDK is released.

Download: http://unitdriven.codeplex.com/releases/view/50214

Here is a screenshot of UnitDriven running tests on the WindowsPhone7 emulator.

UnitDrivenPhone

One interesting thing is that all I had to do to support this was link files from the Silverlight version of UnitDriven into a new Phone project. It all compiled and ran on the first try.

However, even though it ran it wasn’t actually usable. I had to create new versions of the Views to accomodate the smaller screen size and default layout differences (buttons are relatively bigger for example). Also the scroll bars are only visible while scrolling and you have to click and drag on a Circle or the text to actually do the scrolling.

Please feel free to comment on the UnitDriven forums if you have any comments or questions!

UnitDriven v0.0.5 Available

http://unitdriven.codeplex.com/releases/view/46068

I applied some updates to UnitDriven and released a new version recently. The updates provide some nicer nesting of namespaces in the test runner UI as well as improved disabling of ‘Run’ buttons and correctly functioning Timeouts.

Also the update is paired with updates to StatLight so you can run your Silverlight unit tests in a purely automated fashion.

Also, if you find yourself trying to remember why you would want to use UnitDriven instead of one of the other unit test frameworks for Silverlight, here are the main features.

  • First class support for asynchronous tests.
  • Supports the ability to author identical tests for Silverlight and .NET (file linking).
  • Parallels either MSTest or NUnit seamlessly.

Silverlight Timer Woes

I was just encountering a bug with using the System.Threading.Timer object in Silverlight for UnitDriven. It was very non-intuitive and sporadic so I thought I’d post some of my conclusions here just in case.

The problem was, that if I had a test that contained a BackgroundWorker that called Thread.Sleep(x) in it’s DoWork event, the timer callback would never get called. I would set the timer to fire in 5 seconds then call sleep for 30. The timer would only fire after the sleep was concluded.

I believe this is the case because Silverlight only has 1 background thread, or at least the Timer and the BackgroundWorker are sharing the same thread. So even though the timers timeout has expired it cannot get CPU time to do the work and fire the callback. The solution I came up with was to, instead of using a timer, simply call Application.Current.RootVisual.BeginInvoke(this.CheckForTimeout) and do the calculation myself. That worked like a charm.

So my conclusion is that getting yourself onto the UI thread in Silverlight is a more reliable way of guaranteeing CPU time since the UI thread is more likely to pumping continuously. Of course too much of that and everything will seem sluggish. What I really need is a backround work queue, or a dispatcher for a background thread in other words.