The PDC trivia application (more generically called the Event
Trivia or Conference Trivia app) is a full-screen web-delivered
Silverlight application that displays trivia questions on the big
screen in the time between breakout sessions at a conference. So
far, it has been used at PDC09, MIX10, and PDC10.
The previous versions had only trivia questions,
and a very conference-themed display. The latest version, rewritten
for PDC10, has a Halloween theme in recognition of the timing of
the conference. The overall architecture for the runtime trivia
portion hasn't changed, but the implementation details have. In
addition, for the PDC2010 version, I added Twitter integration with
tweets popping up as tombstones on the bottom half of the
application.
One other change is the removal of the images from the
questions. While the database still supports images, I removed them
in order to avoid any potential copyright issues (including images
I purchased from various sites) which would prevent releasing this
application on codeplex.
Overall Architecture
The Silverlight application follows a typical Silverlight
application pattern: WCF service on an ASP.NET web site, database
behind that. External services (search.twitter.com in this case)
called directly from the Silverlight client.
The Silverlight client itself uses a basic MVVM pattern
approach. I'm not using any specific toolkit, nor was I religious
about it (there is a little bit of code in the code-behind). I
unapologetically did what made sense in the few hours I had to
develop this new version over the course of a few days. :) That
said, the overall architecture is solid.
Why did I pick Silverlight? I wanted to have a lot of freedom
for the experience, and I needed something that would install
without any trouble. Since the PDC machines often include futures
and side builds to show proofs-of-concept, I couldn't count on
specific versions of .NET. I could, however, count on Silverlight 4
:)
Let's take a deeper look into the layers of this app, starting
with the XAML user interface.
User Interface
The majority of the user interface was done with bitmap images
(jpeg and pngs) created in Photoshop. The grass was created with a
couple of CS5 brushes that look like, well, grass. I desaturated
the color, then added a layer (render->clouds) which I used to
modify the base layer to create some fog. I hand-edited the fog to
get the look I was going for. The background is typical gradient
with some manually-added stars.
I then added some mist. My intent was to have this mist roll
across the screen, but I didn't get a chance to implement that
animation. To do that, I would create a much wider version of the
mist and make sure it nicely trails off on the right.
Finally, I have the tombstone graphics, broken into two pieces.
The first piece is the tombstone itself. This is what pops up from
the grass. The right is just enough grass to cover the base of the
tombstone. This is used to hide the base of the tombstone as well
as provide something to cover the hard clipping edge used to allow
the tombstone to pop up.
The two images are separate layers in the same Photoshop
file.
Displaying the Tombstones
The tombstones are surfaced from the viewmodel as an
ObservableCollection of Tweet objects. The Tweet object contains
the tweet id, timestamp, sender, message and (although unused) the
url of the sender's avatar:
// TwitterService
private ObservableCollection<Tweet> _tweets = new ObservableCollection<Tweet>();
public ObservableCollection<Tweet> Tweets
{
get { return _tweets; }
}
// ViewModel
public ObservableCollection<Tweet> Tweets
{
get { return _twitterService.Tweets; }
}
You'll notice that this is just a pass-through from the
TwitterService class. The TwitterService class is responsible for
polling twitter, and loading up the latest tweets. It's also
responsible for semi-randomizing what shows up, but I know from
feedback that I fell down a bit on the randomization in the PDC10
version. The version on codeplex will have that code updated.
The TwitterService class has a timer which makes a call to
search.twitter.com every 60 seconds (30 would be better) and loads
the latest tweets. Those are then put into the observable
collection. Binding on the client takes care of the rest. In fact,
the XAML UI for the tombstones in MainPage.xaml is pretty
simple:
<Grid x:Name="TwitterFeed" Margin="50 300 50 0">
<ItemsControl x:Name="TweetList"
ItemsSource="{Binding Tweets}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<localControls:GraveyardPanel ItemNearScale="1.50" BackgroundPositionJitter="35" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<localControls:Tombstone />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
In that, you can see that I have an ItemTemplate which contains
a reference to the Tombstone usercontrol. I also have a custom
GraveyardPanel which is responsible for positioning the tombstones
and showing one larger than the rest. The source for that will be
included in the codeplex upload.
The use of the custom panel allows me to use a regular old
ItemsControl. Yep, all seven tombstones (I could have done 9 on
that 720p screen, I realized) are displayed using that single
ItemsControl and the GraveyardPanel.
While the GraveyardPanel is responsible for positioning (which
has some randomization built-in) the Tombstone control itself is
responsible for canting the stone one way or the other, and
displaying it after a random delay. The XAML for the tombstone
usercontrol looks like this:
<UserControl x:Class="PeteBrown.ConferenceTrivia.Controls.Tombstone"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="874" d:DesignWidth="695">
<UserControl.Resources>
<Storyboard x:Key="ShakeAndAppear">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)"
Storyboard.TargetName="grid">
<EasingDoubleKeyFrame KeyTime="0"
Value="1000">
<EasingDoubleKeyFrame.EasingFunction>
<ElasticEase EasingMode="EaseOut"
Oscillations="1"
Springiness="10" />
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<EasingDoubleKeyFrame KeyTime="0:0:2"
Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<ElasticEase EasingMode="EaseOut"
Oscillations="1"
Springiness="10" />
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)"
Storyboard.TargetName="grid">
<EasingDoubleKeyFrame KeyTime="0"
Value="50"
EasingFunction="{x:Null}" />
<EasingDoubleKeyFrame KeyTime="0:0:2"
Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<ElasticEase EasingMode="EaseOut"
Oscillations="15"
Springiness="7" />
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</UserControl.Resources>
<Grid x:Name="LayoutRoot">
<Viewbox>
<Grid Width="695"
Height="874">
<Grid>
<Grid.Clip>
<RectangleGeometry Rect="0,-20,695,794" />
</Grid.Clip>
<Grid x:Name="grid"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<CompositeTransform TranslateY="1000" />
</Grid.RenderTransform>
<Image Source="../Assets/Tombstone1.png" />
<Grid Margin="115 90 115 200">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{Binding Author}"
Style="{StaticResource TweetAuthorStyle}">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="15"
Color="Black"
ShadowDepth="0" />
</TextBlock.Effect>
</TextBlock>
<TextBlock Grid.Row="1"
Text="{Binding Message}"
Style="{StaticResource TweetMessageStyle}">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="10"
Color="Black"
ShadowDepth="0" />
</TextBlock.Effect>
</TextBlock>
</Grid>
</Grid>
</Grid>
<Image Source="../Assets/Tombstone1_Grass.png" />
</Grid>
</Viewbox>
</Grid>
</UserControl>
There's a bunch there, but if you saw the tombstones in action,
you'd have noticed:
- They scale based on resolution
- They randomly cant a number of degrees either left of
right
- After a short, random delay, they pop up from the grass (this
involves clipping and some animation)
The ShakeAndAppear animation is a total cheat :) To get the
shaking effect, rather than keyframe a true shake, I used a tight
ElasticEase with a higher than usual number of iterations. The
animation also moves the tombstone up so it appears within the
clipping rectangle. Scaling is handled by the ViewBox.
The display of the tombstone is relatively straight forward. The
initial state of the usercontrol is as shown in the left of the
image below. The blue rectangle is the clipping rectangle for the
tombstone (the grass sits in front of it, outside the clip). When
the time comes to appear, the tombstone's Y offset is animated from
1000 to 0 with some easing to give it a little bounce. This brings
it within the clipping bounds.
Forcing Full-Screen Mode
When you fist run the application, the displayed UI is just a
background with a button asking you to enter full-screen mode. This
is handled by grouping the page content into two containers, and by
handling the FullScreenChanged event in the code-behind.
<Grid x:Name="RunningContent"
Visibility="Collapsed">
<Grid x:Name="Trivia"
...
</Grid>
<Grid x:Name="TwitterFeed" Margin="50 300 50 0">
<ItemsControl x:Name="TweetList"
...
</ItemsControl>
</Grid>
...
</Grid>
<Grid x:Name="OpeningContent">
<Rectangle Fill="Black" />
<TextBlock Text="PDC 2010 Trivia by Pete Brown"
Style="{StaticResource HashTagStyle}"
HorizontalAlignment="Center"
Margin="0 200 0 0"/>
<Button Content="Click here for Full Screen"
Height="30"
Width="175"
Click="Button_Click" />
</Grid>
The part of the code-behind that integrates with that looks like
this:
public MainPage()
{
...
App.Current.Host.Content.FullScreenChanged += new EventHandler(Content_FullScreenChanged);
}
void Content_FullScreenChanged(object sender, EventArgs e)
{
if (App.Current.Host.Content.IsFullScreen)
{
RunningContent.Visibility = Visibility.Visible;
OpeningContent.Visibility = Visibility.Collapsed;
}
else
{
RunningContent.Visibility = Visibility.Collapsed;
OpeningContent.Visibility = Visibility.Visible;
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
App.Current.Host.Content.IsFullScreen = true;
}
When the button is clicked, the application is placed into
full-screen mode. That, in turn, fires the FullScreenChanged event.
The same thing happens when the user hits escape to exit
full-screen mode.
Deployment
At every conference where I use this, I request a small server.
This is usually a desktop or workstation-class box with 8gb memory
and Windows Server 2008 r2. On it I install .NET 4 and SQL Server
Express 2008. The database is in the app_data folder of the
site.
Once the site is set up and tested (I deploy everything via
remote desktop), the server is ready to be packed and shipped to
the facility. Once there, I usually do a smoke test to make sure
everything is still working.
The room staff know to simply hit the URL for the machine and
click the "full screen" button. It is super simple for them to use,
which definitely adds to the success of the project.
Summary
There's more interesting stuff going on in the code. Rather than
just paste it all in here, I figured I'll shortly release the
source on codeplex at http://eventtrivia.codeplex.com
so anyone can use it, modify it, or just take a look.
If you want to use this in your own events, please feel free to
do so. In fact, I encourage you to use this, and add to the store
of trivia questions. I'd love it if you mentioned somewhere obvious
that you're using the app I wrote, but that's neither necessary nor
a condition of use. Do tell me if you use it, though, and take
pictures! :)
Update 2010-11-22: This is now published at http://eventtrivia.codeplex.com/