Thursday, January 27, 2011

Mastering Storyboards One Mistake at a Time

I've always been interested in animation. I wrote over 100 programs in QBASIC and Turbo C++ as a teenager where I was creating various animations and exploring new techniques. Then I graduated to newer technologies like OpenGL, Flash, and much later -- Silverlight.

I almost became a game programmer, and probably would have if I didn't have to relocate. But anyway, as time goes on you begin to understand what tools to use and when. And obviously animation is necessary in today's video games, but it's not something that should be overused in other applications. Still, I contend that when used in moderation, it can be a nice finishing touch.

Storyboards

Storyboards in Silverlight are a simple, declarative way to define animation sequences. They can be used to animate just about any property on a UIElement.

I'm going to walk through a few Storyboard examples that build upon eachother while pointing out several different techniques (some better than others). The goal is not to cover every technical possibility with Silverlight. Rather, it's to intentionally run into common technical, design, or usability mistakes and finally arrive at something that's pretty good.

In my examples below I chose to animate the Opacity property. The Opacity property is simply a double value that accepts any number between 0 (transparent) and 1 (opaque).

I'm going to create a set of images and captions, where both the image and caption will fade their opacity to 1 (opaque) as you mouseover them. I also want them to be clickable to open a bigger image.

Example 1


This first attempt is simple and straightforward.

First, I create my images and captions in XAML:

<Grid x:Name="LayoutRoot" Background="Black">
 <StackPanel HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10">
  <StackPanel Orientation="Horizontal">
   <Image x:Name="Image1" Opacity="0.5" Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/011/788697398_xPrbE-Tiny.jpg"/>
   <TextBlock x:Name="Caption1" Opacity="0.5" VerticalAlignment="Center" Width="250" Text="I arrived on Santa Cruz Island and after just a few minutes I stumbled across this view." TextWrapping="Wrap" />
  </StackPanel>
  <StackPanel Orientation="Horizontal">
   <Image x:Name="Image2" Opacity="0.5" Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/136/789417908_3bjhd-Tiny.jpg"/>
   <TextBlock x:Name="Caption2" Opacity="0.5" VerticalAlignment="Center" Width="250" Text="But there was much more coast and wildlife to see, so I hiked for hours." TextWrapping="Wrap" />
  </StackPanel>
  <StackPanel Orientation="Horizontal">
   <Image x:Name="Image3" Opacity="0.5" Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/088/789378275_maquH-Tiny.jpg"/>
   <TextBlock x:Name="Caption3" Opacity="0.5" VerticalAlignment="Center" Width="250" Text="Then I relaxed a bit." TextWrapping="Wrap" />
  </StackPanel>
 </StackPanel>
</Grid>

Then, I create storyboards for each:

<UserControl.Resources>
  <Storyboard x:Name="ImageRollover1">
   <DoubleAnimation Storyboard.TargetName="Image1" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption1" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollout1">
   <DoubleAnimation Storyboard.TargetName="Image1" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption1" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollover2">
   <DoubleAnimation Storyboard.TargetName="Image2" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption2" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollout2">
   <DoubleAnimation Storyboard.TargetName="Image2" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption2" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollover3">
   <DoubleAnimation Storyboard.TargetName="Image3" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption3" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
  </Storyboard>
  <Storyboard x:Name="ImageRollout3">
   <DoubleAnimation Storyboard.TargetName="Image3" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
   <DoubleAnimation Storyboard.TargetName="Caption3" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
  </Storyboard>
 </UserControl.Resources>

And as a last step I wire the MouseEnter and MouseLeave events in the codebehind:
public MainPage()
  {
   InitializeComponent();

   Image1.MouseEnter += Image1_MouseEnter;
   Image1.MouseLeave += Image1_MouseLeave;

   Image2.MouseEnter += Image2_MouseEnter;
   Image2.MouseLeave += Image2_MouseLeave;

   Image3.MouseEnter += Image3_MouseEnter;
   Image3.MouseLeave += Image3_MouseLeave;
  }

  private void Image1_MouseEnter(object sender, MouseEventArgs e)
  {
   ImageRollover1.Begin();
  }
  private void Image1_MouseLeave(object sender, MouseEventArgs e)
  {
   ImageRollout1.Begin();
  }

  private void Image2_MouseEnter(object sender, MouseEventArgs e)
  {
   ImageRollover2.Begin();
  }
  private void Image2_MouseLeave(object sender, MouseEventArgs e)
  {
   ImageRollout2.Begin();
  }

  private void Image3_MouseEnter(object sender, MouseEventArgs e)
  {
   ImageRollover3.Begin();
  }
  private void Image3_MouseLeave(object sender, MouseEventArgs e)
  {
   ImageRollout3.Begin();
  }

And here's the result:

Get Microsoft Silverlight

These pictures came from a trip to California last year to attend an SEO course by Bruce Clay (but I also made a vacation out of it).

Anyway, as you can see it does work. However, there are a few problems:
  1. The mouseover effect only works over the image.
  2. There's a lot of redundant code here. All of those storyboards are identical except for their TargetNames.
  3. With all that code, the images are still not clickable.


Example 2

So let's get a little more sophisticated with our approach. There's a built-in control that can help us. It's the HyperlinkButton.

Go ahead and add a HyperlinkButton to the page. And then right-click on it and click "Edit Template..." > "Edit a copy..."

(To do this you'll need Expression Blend. If you don't have it, don't worry, I'll still post the resulting XAML below.)

After doing that and removing and changing a few things, I end up with this:
<UserControl.Resources>             
        <Style x:Key="HyperlinkButtonRollover" TargetType="HyperlinkButton">
         <Setter Property="Foreground" Value="#FFF"/>
         <Setter Property="Padding" Value="0"/>
         <Setter Property="Cursor" Value="Hand"/>
         <Setter Property="Background" Value="Transparent"/>
         <Setter Property="Template">
          <Setter.Value>
           <ControlTemplate TargetType="HyperlinkButton">
            <Grid Background="{TemplateBinding Background}" Cursor="{TemplateBinding Cursor}">
             <VisualStateManager.VisualStateGroups>
              <VisualStateGroup x:Name="CommonStates">
               <VisualState x:Name="Normal">
                <Storyboard>
                 <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" From="1" To="0.5" Duration="00:00:00.750000" />
                </Storyboard>
               </VisualState>
               <VisualState x:Name="MouseOver">
                <Storyboard>
                 <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" From="0.5" To="1" Duration="00:00:00.750000" />
                </Storyboard>
               </VisualState>
              </VisualStateGroup>
             </VisualStateManager.VisualStateGroups>
             <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
            </Grid>
           </ControlTemplate>
          </Setter.Value>
         </Setter>
        </Style>
    </UserControl.Resources>

One of the cool things about this approach is the VisualStates built into the HyperlinkButton. By using the "Normal" and "MouseOver" states, I can actually get rid of all that code we had added earlier in the codebehind.

And then the XAML for the images and captions looks like this:
<Grid x:Name="LayoutRoot" Background="Black">
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10">
         <HyperlinkButton Style="{StaticResource HyperlinkButtonRollover}" NavigateUri="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/011/788697398_xPrbE-L.jpg" TargetName="_blank">
          <HyperlinkButton.Content>
           <StackPanel Orientation="Horizontal">
                  <Image Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/011/788697398_xPrbE-Tiny.jpg"/>
                  <TextBlock VerticalAlignment="Center" Width="250" Text="I arrived on Santa Cruz Island and after just a few minutes I stumbled across this view." TextWrapping="Wrap" />
              </StackPanel>
    </HyperlinkButton.Content>
         </HyperlinkButton>
   <HyperlinkButton Style="{StaticResource HyperlinkButtonRollover}" NavigateUri="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/136/789417908_3bjhd-L.jpg" TargetName="_blank">
          <HyperlinkButton.Content>
           <StackPanel Orientation="Horizontal">
                  <Image Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/136/789417908_3bjhd-Tiny.jpg"/>
                  <TextBlock VerticalAlignment="Center" Width="250" Text="But there was much more coast and wildlife to see, so I hiked for hours." TextWrapping="Wrap" />
              </StackPanel>
    </HyperlinkButton.Content>
         </HyperlinkButton>  
   <HyperlinkButton Style="{StaticResource HyperlinkButtonRollover}" NavigateUri="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/088/789378275_maquH-L.jpg" TargetName="_blank">
          <HyperlinkButton.Content>
           <StackPanel Orientation="Horizontal">
                  <Image Height="100" Width="100" Margin="10,1" Source="http://swortham.smugmug.com/Vacation/Santa-Cruz-Island-2010/088/789378275_maquH-Tiny.jpg"/>
                  <TextBlock VerticalAlignment="Center" Width="250" Text="Then I relaxed a bit." TextWrapping="Wrap" />
              </StackPanel>
    </HyperlinkButton.Content>
         </HyperlinkButton>  
        </StackPanel>
    </Grid>

Get Microsoft Silverlight

With those modifications the XAML is looking a little neater and the app is more or less doing what I want.

The remaining problems are those of design and usability:
  1. If you mouseover every image very quickly you'll notice that they kind of flash abruptly. This is due to an oversight in our storyboards which we'll fix in the next example.
  2. The animations have a long duration. They are currently set at 0.75 seconds. That doesn't sound like a long time, but it feels like it when you're actually using the app. Slow transitions like this are fine in a loading screen, where the user can't do anything but wait. But when they're actually using your application, a slow animation can give the impression that your app is slow and unresponsive. The user may even feel like they must let the animation play in its entirety before clicking.
  3. The animation feels a bit robotic and unnatural.


Example 3

First we'll address the abrupt flashing we were experiencing. The problem occurs when you mouse over and then off of an element before its mouseover animation is done playing.

The problem is incredibly easy to solve, although the solution may not be obvious at first. All you have to do is remove the "From" attribute from the storyboards...

The "From" was causing the MouseOver and Normal states to always start at a specified Opacity. In reality, we simply don't care at what Opacity the animation starts. We only care that it ends.

Next I'll change the duration of the storyboards to 0.3 seconds for the "MouseOver" state and 0.5 seconds for the "Normal" state.

And last I'm going to specify some easing. Learning how to use easing makes the difference between robotic/abrupt feeling transitions, and a smooth user interface. I'm going to work with one of the simpler EasingFunctions, the CircleEase. And I'll use the EaseOut mode. By doing this, the majority of the animation occurs up front and then settles gradually. It's a subtle but noticeable difference.

The resulting changes were all in the Storyboards:
<UserControl.Resources>
        <Style x:Key="HyperlinkButtonRollover" TargetType="HyperlinkButton">
            <Setter Property="Foreground" Value="#FFF"/>
            <Setter Property="Padding" Value="1"/>
            <Setter Property="Cursor" Value="Hand"/>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="HyperlinkButton">
                        <Grid Background="{TemplateBinding Background}" Cursor="{TemplateBinding Cursor}">
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" To="0.5" Duration="00:00:00.500000">
                                             <DoubleAnimation.EasingFunction>
                                              <CircleEase EasingMode="EaseOut" />
            </DoubleAnimation.EasingFunction>
                                            </DoubleAnimation>          
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="MouseOver">
                                        <Storyboard>
                                            <DoubleAnimation Storyboard.TargetName="contentPresenter" Storyboard.TargetProperty="Opacity" To="1" Duration="00:00:00.300000">
                                             <DoubleAnimation.EasingFunction>
                                              <CircleEase EasingMode="EaseOut" />
            </DoubleAnimation.EasingFunction>
                                            </DoubleAnimation>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

And the final app is here...
Get Microsoft Silverlight


I've packaged all of the projects above into one zip file...
Download MasteringStoryboards.zip



Bonus: Building it all into a Control

I thought it might be cool to animate the image and the caption separately, as if the image is sliding over and bumping into the caption. To do this while maintaining everything the HyperlinkButton gives us I've created a new custom control called the Captioned Image Hyperlink.

Tuesday, January 25, 2011

XAP File Optimization Techniques for 2011

So you've created something cool in Silverlight but your XAP file is bigger than you would've hoped. What can you do about it?

Well there are tips to be found on the internet, but much of what you'll find is outdated. Some say that recompressing the XAP file with something like WinRAR on its "best" compression setting will help. But that was in the Visual Studio 2008 / Silverlight 2 days. Now if you're building a Silverlight 4 app in Visual Studio 2010 it's already going to be compressed about as heavily as is possible.

So I'm going to talk about other effective techniques that will help you today.

Tips


1.) Reduce XAP size by using application library caching (not available for out-of-browser applications). Check this option in Visual Studio 2010 and when your project is built there will be extra files automatically generated. So you'll have to upload your XAP file and any auto-generated ZIP files to the same directory. The generated ZIP files contain any external assemblies that your XAP require. Behind the scenes this relationship is automatically maintained with the AppManifest. This feature is pretty cool and very easy to use. You should note, however, that the whole idea behind it is caching. And so it speeds up requests from repeat visitors, but in practice it doesn't do a thing to speed up the experience for a new visitor. Read more about this feature from Microsoft.

2.) Make sure that you're including references to assemblies that you actually need and nothing more. Sometimes you'll need a reference to an assembly but you don't want it set to "Copy Local", as that adds to the XAP. Visual Studio 2010 is usually good about automatically setting "Copy Local" when appropriate. For example, System.Windows.Controls is not part of the runtime (it's a part of the SDK). Visual Studio knows this and when you add System.Windows.Controls to the project it automatically sets "Copy Local" to true. But when in doubt about an assembly reference, try setting "Copy Local" to false and see what happens. If your application builds and runs fine, then you're good.

3.) If you have a need for just one of these controls below, then download and use it rather than including the entire System.Windows.Controls assembly:
Calendar
Date Picker
Grid Splitter
Tab Control
I ripped those controls out of the Silverlight SDK a couple weeks ago, and created individual projects out of them. If you only needed the Grid Splitter, for example, then you could save around 60 KB by adding it to your project instead of the entire System.Windows.Controls.dll.

4.) Set up content expiration in IIS. So far we've just been concentrating on the XAP file itself. It's easy to forget about server configuration. Just like Tip #1, this is all about improving the experience for repeat visitors (and reducing server load of course).

In IIS7, follow these steps:
  1. Browse to the ClientBin folder (or wherever you XAP file(s) are stored)
  2. Open "HTTP Response Headers"
  3. Click "Set Common Headers..."
  4. Check "Expire Web content" and select "After" some time.  Sometimes I'll set it for 1 hour, or sometimes 1 day depending on how important it is that the application remains up-to-date.



Tools


1.) XAPs Minifier Visual Studio 2010 plug-in
As mentioned in tip #2, sometimes when working with a Silverlight app you might have an assembly reference that has been set to "Copy Local" when it doesn't need to be. This is easy to overlook when working with a large Silverlight app composed of multiple projects. The XAPs Minifier is intended to remove any redundant or unnecessary assemblies.

2.) ComponentOne XAPOptimizer
The XAPOptimizer is not free, but it does take optimization to another level. It goes beyond assembly removal. It'll dig into every assembly through reflection and actually detect unnecessary classes, resources and entire sections of XAML. It's also configurable in case it makes a mistake you can actually specify what to remove and save your settings. On top of this, it'll optionally obfuscate your application, making it hard to reverse-engineer.


Conclusion

Often the most significant optimizations involve removing unnecessary or unused components or assemblies. With a little diligence, you'll be surprised what a difference you can make.

kick it on DotNetKicks.com

Monday, January 24, 2011

SilverlightXAP March 2011 Developer Contest

After launching SilverlightXAP last Thursday, we're looking for Silverlight developers willing to build & sell Silverlight controls.  So I'm creating a contest where the best Silverlight developers are rewarded.  Whoever has the most total sales this March will receive a cash prize in addition to their profits from the site.

1st place
$1,000
2nd place
$500
3rd place
$250



  • Prizes will be awarded based off of total sales (in dollars) from each developer.
  • The winners will be announced April 1st, 2011.

Note that this contest is only for the month of March 2011, and assumes that at least three developers have sales in March.  But ideally you'd want to release your control(s) in February to give it a chance to build some momentum.  Sign up to begin.

Saturday, January 22, 2011

The Silverlight SDK and XAP File Sizes

Microsoft shipped a variety of useful controls with Silverlight.  Some are built into the runtime, while others are in the SDK.  While the use of runtime controls has no substantial penalty in your XAP file size, if you want to use any controls from the SDK you have to include the System.Windows.Controls assembly or one of its sub-assemblies.

As of today, this is the final (XAP compressed) overhead that you can expect from each assembly:

90 KB   - System.Windows.Controls
216 KB - System.Windows.Controls.Data
48 KB   - System.Windows.Controls.Data.Input
121 KB - System.Windows.Controls.Input
28 KB   - System.Windows.Controls.Navigation


In 2009 when I was developing Regex Hero I was determined to keep it very lightweight.  Most of the controls I was using were from the Silverlight runtime.  In fact the only one that I was using from the SDK was the GridSplitter.  But I wanted to know how I could include the GridSplitter in my project without including the full System.Windows.Controls.dll from the SDK.  Turns out that Microsoft released the source code for the SDK and runtime controls from Silverlight 2.  Long story short, I was able save 60 KB from Regex Hero by stripping the GridSplitter out into its own control and using it instead.  After that, Regex Hero was only 51 KB.

In total, I've stripped out these 4 controls from System.Windows.Controls:


Just as I did, you can download any of the controls above to eliminate your need for the System.Windows.Controls assembly and reduce your XAP file size.  However, as a rule I'd say that if you need more than 2 of these controls, simply use the System.Windows.Controls assembly instead.  There will be some overlap in these controls and after a certain point it makes more sense to simply use the original assembly.

Note:
There are a lot more controls in the various other assemblies but not all of the source code is available from Microsoft.  Maybe some day they will release the up-to-date code for the rest of the SDK and I can apply the same treatment to them.