+1  A: 

You could make more than one corner "un-matched" in that regard. For example, instead of having one point be the "source" and "destination" of the animated dashes, you could pick 2 points. One would be the "source", dashes appearing to march away from it in 2 directions, and another point be the "destination", where dashes converge and disappear.

GIMP, for example, animates selection dashed lines in this way and seems to pick a point closest to the lower-left for the "source" and a point closest to the upper-right for the "destination".

You could come up with some other scheme, as well.

Just remember that while it may look disturbing to you, most users will not care.

Joel B Fant
You where right I maybe was a bit to picky. The advice with 2 starting points is also pretty good. Thanks!
chrischu
A: 

I just found a way that makes it way easier to create such an animated SelectionBorder.

Instead of creating the animation by moving an self-created AnimationPoint through animation I just animated the StrokeDashOffset property natively provided by the Shape class and setting the StrokeDashArray to define the StrokeLength.

It would look like this in XAML:

<namespace:SelectionBorder StrokeDashArray="2" AnimationDuration="0:0:1" Stroke="Black" />

The class looks like this:

public class SelectionBorder : Shape
{
 private DoubleAnimation m_Animation;
 private bool m_AnimationStarted;

 public SelectionBorder()
 {
  IsVisibleChanged += OnIsVisibleChanged;
 }

 protected void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
 {
  if (Visibility == Visibility.Visible)
  {
   StartAnimation();
  }
  else
  {
   StopAnimation();
  }
 }

 public void StartAnimation()
 {
  if (m_AnimationStarted)
   return;

  if (m_Animation == null)
  {
   m_Animation = CreateAnimation();
  }

  BeginAnimation(StrokeDashOffsetProperty, m_Animation);
  m_AnimationStarted = true;
 }

 protected virtual DoubleAnimation CreateAnimation()
 {
  DoubleAnimation animation = new DoubleAnimation();
  animation.From = 0;
  if (StrokeDashArray.Count == 0)
   animation.To = 4;
  else
   animation.To = StrokeDashArray.First() * 2;
  animation.Duration = AnimationDuration;
  animation.RepeatBehavior = RepeatBehavior.Forever;
  return animation;
 }

 public void StopAnimation()
 {
  if (m_AnimationStarted)
  {
   BeginAnimation(StrokeDashOffsetProperty, null);
   m_AnimationStarted = false;
  }
 }

 #region Dependency Properties

 public Duration AnimationDuration
 {
  get { return (Duration)GetValue(AnimationDurationProperty); }
  set { SetValue(AnimationDurationProperty, value); }
 }

 public static readonly DependencyProperty AnimationDurationProperty =
  DependencyProperty.Register("AnimationDuration", typeof(Duration), typeof(SelectionBorder), new UIPropertyMetadata(new Duration(TimeSpan.FromSeconds(0.5))));


 #endregion Dependency Properties

 protected override Geometry DefiningGeometry
 {
  get
  {
   double width = (double.IsNaN(Width)) ? ((Panel)Parent).ActualWidth : Width;
   double height = (double.IsNaN(Height)) ? ((Panel)Parent).ActualHeight : Height;

   RectangleGeometry geometry = new RectangleGeometry(new Rect(0, 0, width, height));

   return geometry;
  }
 }
}
chrischu