Friday 28 March 2008

Gel Button Template in Silverlight

A while ago I posted about how to get a gel button effect in Silverlight. With the advent of Silverlight 2.0 beta, it is now possible to turn this into a button template for easy reuse. What's more, it is relatively straightforward to add animations for states such as MouseOver and MouseDown.

The way to go about this is to create a style in your App.xaml file (you can put the style in any XAML page, but putting it in App.Xaml makes it available everywhere in your application).

<LinearGradientBrush x:Key="GreenGradientBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientStop Offset="0" Color="#006700"/>
    <GradientStop Offset="1" Color="#00ef00"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="RedGradientBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientStop Offset="0" Color="#774040"/>
    <GradientStop Offset="1" Color="#ef4040"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="BlueGradientBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientStop Offset="0" Color="#404077"/>
    <GradientStop Offset="1" Color="#4040ef"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="CyanGradientBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientStop Offset="0" Color="#007777"/>
    <GradientStop Offset="1" Color="#00efef"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="YellowGradientBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientStop Offset="0" Color="#777740"/>
    <GradientStop Offset="1" Color="Yellow"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="MagentaGradientBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientStop Offset="0" Color="#770077"/>
    <GradientStop Offset="1" Color="#ef00ef"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="OrangeGradientBrush" StartPoint="0,0" EndPoint="0,1">
    <GradientStop Offset="0" Color="DarkOrange"/>
    <GradientStop Offset="1" Color="Orange"/>
</LinearGradientBrush>

<Style TargetType="Button" x:Key="GelButton">
    <Setter Property="Background" Value="{StaticResource GreenGradientBrush}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Grid x:Name="RootElement">
                    <Grid.Resources>
                        <Storyboard x:Key='MouseOver State'>
                            <DoubleAnimation Storyboard.TargetName='MainButtonScale' 
                                Storyboard.TargetProperty='ScaleX' To='1.03' 
                                Duration='0:0:0.05'/>
                            <DoubleAnimation Storyboard.TargetName='MainButtonScale' 
                                Storyboard.TargetProperty='ScaleY' To='1.03' 
                                Duration='0:0:0.05'/>
                        </Storyboard>
                        <Storyboard x:Key='Normal State'>
                        </Storyboard>
                        <Storyboard x:Key='Pressed State'>
                            <DoubleAnimation Storyboard.TargetName='MainButtonTranslate' 
                                Storyboard.TargetProperty='X' To='1.0' 
                                Duration='0:0:0.05'/>
                            <DoubleAnimation Storyboard.TargetName='MainButtonTranslate' 
                                Storyboard.TargetProperty='Y' To='1.0' 
                                Duration='0:0:0.05'/>
                        </Storyboard>
                        <Storyboard x:Key="Disabled State">                                    
                            <DoubleAnimation Duration="0:0:0" 
                                Storyboard.TargetName="Disabled" 
                                Storyboard.TargetProperty="Opacity" 
                                To="1" />
                        </Storyboard>
                    </Grid.Resources>

                    <!-- drop shadow - needs to be blurred for correct effect -->
                    <Rectangle Fill="#40000000" Margin="1,1,0,0" RadiusX="6" RadiusY="6" />

                    <!-- main rect -->
                    <Grid x:Name="mainButton"
                          Margin="0,0,1,1"
                          RenderTransformOrigin="0.5,0.5">
                        <Grid.RenderTransform>
                            <TransformGroup>
                                <TranslateTransform x:Name="MainButtonTranslate"
                                                X="0.0" Y="0.0"/>
                                <ScaleTransform 
                                x:Name="MainButtonScale"
                                ScaleX="1.0" ScaleY="1.0" 
                                />
                            </TransformGroup>

                        </Grid.RenderTransform>
                        <Rectangle x:Name="mainRect" 
                               Fill="{TemplateBinding Background}"
                               RadiusX="6" RadiusY="6">
                        </Rectangle>

                        <!-- transparent rect -->
                        <Rectangle x:Name="glowRect" Margin="1,1,1,1" RadiusX="5" RadiusY="5">
                            <Rectangle.Fill>
                                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                    <GradientStop Offset="0" Color="#DFFFFFFF"/>
                                    <GradientStop Offset="1" Color="#00FFFFFF"/>
                                </LinearGradientBrush>
                            </Rectangle.Fill>
                        </Rectangle>

                        <ContentPresenter HorizontalAlignment="Center"
                             Margin="4,0,4,0"
                             Content="{TemplateBinding Content}"
                             VerticalAlignment="Center" />

                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>            
</Style>

As you can see, there is a lot of XAML, but it is fairly straightforward once you know what it is. First I have declared a load of LinearGradientBrushes as static resources. These give me an easy way to specify a different colour scheme for the buttons without having to create a whole new template for each one.

Then there is the actual style. We have two properties. The first sets a default background for us - green, but this can be overridden in XAML. The second provides the template. This consists of the visual elements required to draw our gel button, and a number of animations.

The template XAML itself is not really any different from that shown in my original post, but there are a few exceptions. First we are using a "ContentPresenter" to show the text or image that the button will contain. Second, we are using TemplateBinding to set properties such as Background, Foreground etc. And finally, since we are using Grids, we only need to use Margins to position things correctly, rather than specifying sizes.

The animations are a basic scale for mouse-over which makes the buttons larger, and a translate for pressed. Interestingly, pressing the button seems to cancel the mouse-over animation, which actually has a nice visual effect so I am OK with that. Centring the scale transform was the trickiest part. I tried for ages to get a Converter working that would use a TemplateBinding on Width and Height to divide them by two, but in the end I was helped out on the Silverlight forums with a much easier solution. Simply set the RenderTransformOrigin on the grid to "0.5,0.5" and the scale operates in the middle.

To create one of these buttons is trivially simple. This example uses the GelButton style and changes the Background to Blue.

<Button x:Name="test3" 
   Margin="2" 
   Style="{StaticResource GelButton}" 
   Background="{StaticResource BlueGradientBrush}" 
   Height="24" Width="100" 
   Content="Hello World 3" />

There are some improvements that still need to be made before my GelButton templates are complete

  • Better choice of gradient colours
  • Implement disabled state
  • Implement focus Rectangle
  • Mouse-over effect should be made a little more subtle
  • Shadow effect on the content presenter?

Here's what it looks like:

Friday 21 March 2008

Selecting Custom Objects from LINQ

I have been teaching myself WPF and LINQ to XML to objects by working on a very simple game for my children helping them to learn their "key words" and do some basic maths.

I have the key words stored in a fairly simple XML structure:

<?xml version="1.0" encoding="utf-8" ?>
<KeyWords>
   <Group Name="Level 1">
     <KeyWord>and</KeyWord>
     <KeyWord>cat</KeyWord>
     <KeyWord>he</KeyWord>
     <KeyWord>I</KeyWord>
...

and a KeyWord class to store them in.

class KeyWord 
{
     public string Word { get; set; }
     public string Group { get; set; }
}

The foreach way of parsing the XML is simple enough:

foreach (XElement group in xraw.Elements("Group")) 
{
     foreach (XElement word in group.Elements("KeyWord"))
     {
         wordsList.Add(new KeyWord() 
             { Word = word.Value, 
               Group = group.Attribute("Name").Value });
     }
}

But I wanted to remove the nested foreach loops and do it in one simple LINQ query. It took me a little while to work out how it was done, but once you know it is very simple:

keyWords = (from keyWord in 
    xraw.Elements("Group").Elements("KeyWord")
    select new KeyWord 
        { Word = keyWord.Value, 
          Group = keyWord.Parent.Attribute("Name").Value });

and that's all there is to it. Remarkably simple.

Thursday 20 March 2008

XAML Animation Differences Between WPF and Silverlight 2

I was working recently on adding some basic animations to a WPF application, and assumed I would simply be able to utilise the same syntax that I would if I was doing the same animation in Silverlight. Wrong!

I started by adding a couple of Storyboards to a Grid.Resources section:

<Page x:Class="LearningGames.NumbersPage"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     Title="NumbersPage" Height="260">
     <Grid>
         <Grid.Resources>
          <Storyboard x:Name="storyboardWrong">
             <DoubleAnimation x:Name="da1" Storyboard.TargetName="labelWrong" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.5" />
             <DoubleAnimation x:Name="da2" Storyboard.TargetName="labelWrong" Storyboard.TargetProperty="Opacity" From="1" To="0" BeginTime="0:0:1" Duration="0:0:0.5" />
         </Storyboard>
         <Storyboard x:Name="storyboardRight">
             <DoubleAnimation x:Name="da3" Storyboard.TargetName="labelRight" Storyboard.TargetProperty="Opacity" From="0" To="1" Duration="0:0:0.5" />
             <DoubleAnimation x:Name="da4" Storyboard.TargetName="labelRight" Storyboard.TargetProperty="Opacity" From="1" To="0" BeginTime="0:0:1" Duration="0:0:0.5" />
         </Storyboard>
         </Grid.Resources>

In Silverlight 2, this would be enough. You could simply call storyboardWrong.Begin() in the code behind. But in WPF, things are a bit more complicated:

  • You need to specify x:Key for each Storyboard
  • The XAML compiler doesn't auto-generate member variables for each Storyboard.
  • Storyboards must be found using TryFindResource:
    storyboardWrong = (Storyboard)TryFindResource("storyboardWrong");
  • TryFindResource will only work if the Storyboard is in the Page.Resources section of the XAML
  • When you get round to running the Storyboard, it will need a parameter to the begin method. You can pass the "this" pointer:
    storyboardWrong.Begin(this);

So rather counter-intuitively, Silverlight actually makes some things easier than its more fully featured desktop counterpart WPF. Or am I missing some easier way of doing animation in WPF?

Monday 10 March 2008

Converting SilverlightAudioPlayer to Silverlight 2.0 Beta 1

I've now got round to revisiting my Silverlight Audio Player control and updating it for Silverlight 2.0 beta 1. The addition of a controls library into Silverlight 2.0 meant that I could ditch a whole load of code straight away.

MediaElement

I also noticed some nice improvements to MediaElement. CurrentState is now an enumeration and the Attributes property has been changed to a dictionary. Unfortunately though, Attributes is still not populated with anything when you load an MP3 file. I had been hoping for it to read the ID3 tags.

Custom Templates

Silverlight Audio Player

To implement the position slider, I made a user control consisting of a green rectangle with a slider on top. The rectangle is used to indicate download progress, and so can be adjusted in width. The slider sets the position

Having done this, I wanted to change the appearance of the slider so I got hold of the default templates for the slider control from MSDN and pasted them into my UserControl.Resources. Unfortunately it didn't compile because of an issue with the way that Colors were added as static resources in the template for the Slider's thumb control. Fortunately, I was able to delete the majority of the code as my Thumb control was simply going to be a circle that changed colour with mouse over. The slider background I changed to be a taller transparent rectangle, so that the download progress rectangle could show through from behind and look like it was filling up the progress bar.

Silverlight Audio Player

So it is remarkably easy to supply your own style to a standard control (so long as you have an example to copy!). The next step will be to see whether I can allow a user to completely override the style for my entire SimplePlayer user control, but I am not sure how easy that will be, as Karen Corby's MIX presentation on Silverlight controls (highly recommended) seemed to suggest that styles could only be applied once and that styleable controls needed to be created in a different way (storing their default styles in a generic.xaml file and using attributes to indicate what "parts" they needed).

What's in the XAP?

One of my goals with Silverlight Audio Player is to make it as simple for people to deploy onto their web-sites as possible. The XAP file will go a long way to making this possible. You can see exactly what is in your XAP file by simply renaming it to .zip and loading it into any zip file extractor. This was useful for me, as I had been a little unsure as to whether any of the files in the ClientBin directory were needed in addition to the XAP file.

RectAnimation

Sadly, the RectAnimation I wanted to be able to animate the clip rectangle of my canvas didn't make it into Silverlight 2.0 beta 1. Maybe next time.

Saturday 8 March 2008

More thoughts on Silverlight 2.0 beta 1

I'm continuing to update my SilverNibbles application in an attempt to try out some of the new features in the latest Silverlight beta.

In addition to the things I mentioned in my previous post, here's some things I liked, and some things I learned:

Being able to reference UserControls directly in XAML is brilliant. It has greatly simplified a lot of the code in SilverNibbles.

Intellisense for XAML is great!

The cursor keys finally work properly. Previously they didn't give any key-down events, only key-up.

There is a new DispatcherTimer object that can be used for game loops in preference to having to use an animation of an invisible object.

Buttons can respond to keyboard events even if their container is invisible. You need to set the IsEnabled property to false if you don't want them to be triggered by the space bar or enter key.

Layout panels are nice, but can actually cause a lot of headaches wondering why your user-control is sizing itself in unusual ways. Sometimes Canvas is still the most straightforward thing to use.

The app.xaml file is nice in that it lets you set Style and Template resources. What I would really like to do is something like this:

<Application.Resources> 
<SolidColorBrush x:Name="BackgroundBrush" Color="White" /> 
...

and then in a user control:

<Rectangle Fill={StaticResource BackgroundBrush}/>

I haven't done enough WPF to know whether this would be allowed in WPF, but it would be a very useful feature if you could.

Finally, I have had issues with FireFox and IE not appearing to download the very latest versions of my Silverlight apps from my webserver. I know the latest version is online, but sometimes the previous version appears in my browser. I'm not quite sure exactly why this is happening yet.

Thursday 6 March 2008

Converting SilverNibbles to Silverlight 2.0 beta 1

Everyone knows that Silverlight 2.0 beta 1 was released yesterday, and already DotNetKicks is awash with information about it, so I'll get straight to business.

This beta brings lots of really cool stuff we have wanted for a long time, controls being the most obvious, as well as layout containers and the ability to apply themes. Also I really like the look of the new .xap format allowing much easier deployment.

My task was to update one of my codeplex projects - SilverNibbles, to use Silverlight 2.0 beta 1. SilverNibbles was my first ever Silverlight app, ported from a WinForms game in just a couple of evenings after downloading the Silverlight 1.1 alpha. It is even featured on the gallery on the Silverlight website, although my complete ineptitude at graphic design means that it looks dreadful compared to all the slick applications alongside it.

My first mistake was installing the Silverlight 2.0 beta 1 SDK. You should just install the Silverlight 2.0 beta 1 tools for VS2008 which includes the SDK. I ended up having to uninstall the SDK before I could install the VS2008 tools.

Next task was to load the project into Visual Studio. I didn't want to create a new project and copy files in, so it required a fair amount of hacking around with the csproj file in a text editor, after making a new 2.0 project which I could use to compare differences. The two web pages most helpful to me during this process were Converting Silverlight 1.1 Alpha projects to Silverlight 2 Beta 1 and the breaking changes for Silverlight 2 Beta 1 page on MSDN. Eventually I had the project set up so it would compile to a .xap file.

After this, there were plenty of compile errors I had to work my way through. The one that held me up the longest was that the [Scriptable] attribute is now replaced by [ScriptableType] and [ScriptableMember]. Also, some event handling functions had changed their function signatures, meaning that my events were no longer connected up to the XAML properly.

The biggest change that had to be made was to completely rework the way that my UserControls were constructed. First of all, the XAML had to be set to compile instead of embedding as a resource. Second, the XAML had to be altered to be a UserControl at the top level rather than a Canvas. Third, the code behind needed to be a partial class that inherited from UserControl. Fourth, I needed to call InitializeComponent in the constructor and use the auto-generated member variables rather than loading from an embedded resource stream and using FindByName.

My PauseControl had a lot of errors in the code, but thanks to the new layout containers in Silverlight 2.0 beta, I could just delete the sizing code. I simply put a text-block inside of a border control and resizing was handled automatically.

One thing that has not improved at all in the transition to 2.0 beta is the error reporting. Errors are as vague as ever. Parse errors rarely point you to the true source of the problem. I got a "catastrophic failure" message simply because a Storyboard.TargetName was wrong.

I eventually got my application compiling and running using the auto-generated test page, and here I ran into another strange bug. My snakes were no longer appearing to move. I traced this down to the fact that I was now modifying the PointsCollection of a polyline rather than giving an entirely new set of points to the polyline every time. I tried various functions such as InvalidateLayout, InvalidateMeasure, UpdateLayout on the polyline to persuade it to redraw itself but nothing worked. In the end I found the following solution:

polyline.Points = polyline.Points;

This did the trick nicely, although its not exactly elegant code!

The next hurdle was that my HTML page that hosted the application no longer worked. Thankfully, the new .xap format makes things a lot easier, and eventually I got my new 2.0 beta edition of SilverNibbles running without any need for external js files.

There is one outstanding bug though, that I can't seem to get the keyboard focus to go to my Silverlight control in FireFox, although it works in IE. I'm sure this used to work with the previous version of Silverlight, but I don't know what I can do to fix this. Any javascript gurus got any suggestions for me? Here's the code:

var silverlightControl = document.getElementById('SilverlightControl');
silverlightControl.Content.SilverNibbles.NewGame(players);
silverlightControl.focus();

The final obstacle was getting it uploaded to my web server. The game never appeared. Eventually I figured out that I needed to add a new MIME type for the .xap extension: application/x-silverlight-app. So finally it is ready to go and you can play the SilverNibbles game online.

Next up is to begin properly working on Silverlight Audio Player after too many issues with alpha 1.1 put this on hold.