views:

156

answers:

4

Hi! I am trying to use XAML completely outside WPF, in particular inside an XNA application. So far, I have managed (quite easily, I am surprised to admit) to load some data inside my XNA application from a XAML file. The problems start when I decided that I wanted to animate one of the properties of my class...Nothing happens :(

Here is the main class I load from the XAML file:

[ContentPropertyAttribute("Animation")]
public class Test : FrameworkContentElement
{
  public string Text { get; set; }
  public Vector2 Position { get; set; }
  public Color Color { get; set; }
  public Storyboard Animation { get; set; }

  public static DependencyProperty RotationProperty = 
    DependencyProperty.Register("Rotation", typeof(double), typeof(Test), new PropertyMetadata(0.0));
  public double Rotation { get { return (double)GetValue(RotationProperty); } set { SetValue(RotationProperty, value); } }
}

Here is the XAML file:

<l:Test xmlns:l="clr-namespace:XAMLAndXNA;assembly=XAMLAndXNA"
      xmlns:a1="clr-namespace:System.Windows.Media.Animation;assembly=PresentationFramework"
      xmlns:a2="clr-namespace:System.Windows.Media.Animation;assembly=PresentationCore"
  Text="Testo" Position="55,60" Color="0,255,255,255">
  <a1:Storyboard>
    <a2:DoubleAnimation a1:Storyboard.TargetProperty="Rotation"
                              From="0"
                              To="360"
                              Duration="00:00:10.0"/>
  </a1:Storyboard>
</l:Test>

And here is the loading and animation launching (attempt):

Test test = XamlReader.Load(new XmlTextReader("SpriteBatchStuff.xaml")) as Test;
test.Animation.Begin(test);

I am dying of curiosity :)

A: 

Outside of the normal loop of a WPF application, I doubt there is any way to drive the animation. There may be some class you can push or prod to drive them, but it is likely sealed.

You will probably wind up building your own animation execution engine running on another thread and ensuring the updates happen on your UI thread, which means either finding a way to reuse the Dispatcher or recreating something similar.

This MSDN article may provide some useful information in this endeavor

It's an interesting project... I'd be curious to hear if you succeed!

Ben Von Handorf
A: 

Wow, this is pretty awesome! Unfortunately, it will likely come down to some "update" type of call that is being made in some internal API. And if you don't call it, the animation won't animate ... much like if an XNA game doesn't have the Update method called.

I would very much like more info on how you're doing this and what level of success you're finding. You should write a blog post/article somewhere :-)

Joel Martinez
+3  A: 

Although XAML is independent of WPF, the visual elements aren't. In particular, animation and layout are part of WPF, and depends on the WPF plumbing being present -- through an Application object, a PresentationSource such as a HwndSource, the XBAP PresentationHost.exe, etc.

So you can read in your XAML and get an object graph of a Test object with a child Storyboard object, but that Test object isn't hooked up to the animation or layout engines until it's placed in a WPF context. All that the XAML gets you is a dumb in-memory object graph: it's WPF, not XAML, that makes the objects "live."

So as Ben says, you'll probably end up needing to "push or prod" the animation yourself. I'm not aware of any documentation on how to do this, but from poking around in Reflector, it looks like the key API is Storyboard.SeekAlignedToLastTick, of which the docs say:

Values are immediately updated to reflect the changes due to SeekAlignedToLastTick, even though the screen does not reflect these changes until the screen updates.

Notice that second clause. Normally, WPF handles the updating of the screen as visual object values change. If you're not using WPF, then it's up to you to read the changed values out and redraw the screen accordingly: you don't have the WPF layout manager to handle it for you.

Finally, please note I haven't tested whether SeekAlignedToLastTick will work in an environment without the WPF plumbing loaded. It sounds like it should, because it doesn't care whether it's WPF or user code which is driving the clocks, but I can't make any promises... though I admit you've got me curious!


UPDATE: I've given this a quick go, and it does seem to work. Here's a demo of hosting an animation within Windows Forms (in this case using a plain ol' Windows Forms timer, but in XNA I guess the framework will provide a game timer for you -- didn't try that because I don't know XNA). Assume you have a vanilla Windows Form with a timer (timer1) and a label (label1), and that the project references the WPF assemblies.

First, my simplified version of your class:

[ContentProperty("Animation")]
public class Fie : DependencyObject
{
  public double Test
  {
    get { return (double)GetValue(TestProperty); }
    set { SetValue(TestProperty, value); }
  }

  public static readonly DependencyProperty TestProperty =
    DependencyProperty.Register("Test", typeof(double), typeof(Fie),
    new FrameworkPropertyMetadata(0.0));

  public Storyboard Animation { get; set; }
}

Now, the WPF code to load one of these babies from XAML and begin the animation:

private Fie _f;
private DateTime _startTime;

public Form1()
{
  InitializeComponent();

  string xaml =
@"<local:Fie xmlns:local=""clr-namespace:AnimationsOutsideWpf;assembly=AnimationsOutsideWpf""
             xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
             xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
             >
  <Storyboard>
    <DoubleAnimation Storyboard.TargetProperty=""Test""
                     From=""0""
                     To=""360""
                     Duration=""00:00:10.0""/>
  </Storyboard>
</local:Fie>";

  _f = (Fie)XamlReader.Load(XmlReader.Create(new StringReader(xaml)));
  Storyboard.SetTarget(_f.Animation, _f);
  _f.Animation.Begin();

  _startTime = DateTime.Now;
  timer1.Enabled = true;
}

Note that I had to set the storyboard's target to be the XAML object I'd just loaded. This doesn't happen automatically. I tried doing this with Storyboard.TargetName in the XAML, but that didn't seem to work -- you may have more luck.

The final lines are just setup for the timer callback:

private void timer1_Tick(object sender, EventArgs e)
{
  TimeSpan sinceStart = DateTime.Now - _startTime;
  _f.Animation.SeekAlignedToLastTick(sinceStart);

  label1.Text = _f.Test.ToString();
}

I've stored the start time of the animation, and used that to calculate how far into the animation we are. WinForms timers are a bit crude, but this suffices for proof of concept; no doubt XNA will have something better. Then I call Storyboard.SeekAlignedToLastTick, which updates the animated values. Nothing displays automatically because my XAML object isn't hooked up for display, but I can check its Test property and verify that it is indeed animating. In reality, I'd use this to update the position or orientation of whatever XNA visual element the XAML object represented.

itowlson
Nicely done! I'm honestly shocked it was that straightforward. I expected there would be a lot of sealed classes in the way.
Ben Von Handorf
+2  A: 

Just for reference, I will now document how I managed to make this work with XNA. Thanks to itowlson for providing the missing link: otherwise I had to create an empty Application with an invisible Window...

We define the class with its animation in XAML (notice the xmlns directives):

<l:Test 
      xmlns:l="clr-namespace:XAMLAndXNA;assembly=XAMLAndXNA"
      xmlns:a1="clr-namespace:System.Windows.Media.Animation;assembly=PresentationFramework"
      xmlns:a2="clr-namespace:System.Windows.Media.Animation;assembly=PresentationCore"
  Text="Testo" Position="55,60" Color="0,255,255,255">
  <a1:Storyboard>
    <a2:DoubleAnimation a1:Storyboard.TargetProperty="Rotation"
                              From="0"
                              To="6.28"
                              Duration="00:00:2.0"
                              RepeatBehavior="Forever"/>
  </a1:Storyboard>
</l:Test>

The "code-behind" class Test is the following:

[ContentPropertyAttribute("Animation")]
public class Test : DependencyObject
{
  public string Text { get; set; }
  public Vector2 Position { get; set; }
  public Color Color { get; set; }
  public Storyboard Animation { get; set; }

  public static DependencyProperty RotationProperty =
    DependencyProperty.Register("Rotation", typeof(double), typeof(Test), new PropertyMetadata(0.0));
  public double Rotation { get { return (double)GetValue(RotationProperty); } set { SetValue(RotationProperty, value); } }
}

In the Initialize function of the XNA Game class we deserialize our xaml file and start the animation:

test = XamlReader.Load(new XmlTextReader("SpriteBatchStuff.xaml")) as Test;
Storyboard.SetTarget(test.Animation, test);
test.Animation.Begin();

The Update function takes as input a GameTime, which offers the TotalGameTime field that stores the TimeSpan of the amount of time passed since the app launch: that is exactly what a Storyboard needs to tick:

protected override void Update(GameTime gameTime)
{
  // Allows the game to exit
  if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
    this.Exit();

  test.Animation.SeekAlignedToLastTick(gameTime.TotalGameTime);

  base.Update(gameTime);
}

In the draw method we can just draw some text using the Rotation property, which will now be correctly animated:

protected override void Draw(GameTime gameTime)
{
  GraphicsDevice.Clear(Color.CornflowerBlue);

  spriteBatch.Begin();
  spriteBatch.DrawString(Content.Load<SpriteFont>("font"), test.Text, test.Position, test.Color, (float)test.Rotation, Vector2.Zero, 1.0f, SpriteEffects.None, 0.0f);
  spriteBatch.End();

  base.Draw(gameTime);
}
Giuseppe Maggiore