Most Microsoft events have a few custom applications built for
them. You never really hear much about them, and perhaps don't even
think about them as custom apps, but there they are, in the keynote
showing tweets, or before sessions (in this case) showing trivia
questions.
MIX has been my favorite conference since I first attended in 2006. Last year I had a
blast and entered the showoff with my C64 emulator in
Silverlight. This will be the first year I get to contribute
something real, albeit small, to each of the breakout sessions in
the conference. This is also the first year I'll attend as a
Microsoft employee.
One of the first projects I worked on when I joined Microsoft in
October 2009, was a small PDC trivia application. This Silverlight 3 +
ASP.NET 4 application ran in the breakout rooms in between
sessions. Scott
Hanselman and I sourced over 200 trivia questions - some
Microsoft-related, some not - to populate the app.
And, since I compiled and ultimately entered all the questions
and answers, you just know there's a fair bit of commodore and
retro trivia in there :)
Here's what the PDC09 version looked like, following the PDC09
presentation theme running at 1280x800. (need to support both
widescreen and 1024x768 for this app)
Today, I updated the application for MIX10. As was the case at
PDC, this will run in the breakout rooms in between sessions.
New MIX10 Version
Here's what the new MIX10 version looks like. Unlike PDC, MIX
doesn't have an enforced style to the content, so I really had
nothing specific to match other than the sites themselves. I may
tweak the UI a bit more before I install it on Sunday, but this is
what it currently looks like when run at 1024x768:
The changes are not huge. I took some styling hints from the MIX Online event
site by using the same colors and creating a brown textured
background (photoshop filter->noise), and moved the countdown to
center screen, but otherwise tried not to create a mess by
pretending to be a designer :)
The real stars in the rooms are the speakers. What I've created
is background for the room while they set up. I was tempted to try
something very Saul Bass like. Maybe if I can get an app into
the keynote room next year :)
Overall Architecture
The application architecture is straight-forward, and fairly
typical for a Silverlight app. The Silverlight client communicates
with the server via a Silverlight-enabled WCF Service. That's just
an easy WCF service implementation that communicates over binary
SOAP.
The Silverlight client itself uses the MVVM or ViewModel pattern
internally. Let's take a look at that.
Silverlight Client Architecture
The Silverlight client architecture is typical for a lightweight
MVVM-based client. The main page is a shell that contains the full
screen button as well as an instance of the Q&A control. The
Q&A control binds to the view-model which in turn makes calls
to the repository to get the data.
The bits in play are:
- Main Page: Shell that holds everything else
- Question and Answer Control: Has storyboards and
datacontext
- Question Control: Visuals for question
- Answer Control: Visuals for answer
- ViewModel: Q&A Control's interface to the rest of the
world
- Shared Entity: The Q&A entity, shared between the client
and WCF
- Repository: Interface to my data.
- WCF Client Proxy: generated client proxy for the WCF
service
The public interface for the view-model looks like this:
public class TriviaViewModel : INotifyPropertyChanged
{
public event EventHandler TriviaLoaded;
public ObservableCollection<TriviaItem> TriviaItems;
public TriviaViewModel(TriviaRepository repository);
public TriviaItem CurrentTriviaItem;
public void MoveFirst();
public void MoveNext();
public void MovePrevious();
public bool IsLoaded;
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
(I'm not using any interfaces in this app; I just removed the
private methods and method bodies so you could see the class
signature.)
Starting it up and Randomizing Questions
When the room monitor hits the url for the site, they get a page
with a single button, asking them to run full-screen. In the
background, it downloads the full list of trivia Q&A. Once they
click that button, the app goes into full-screen mode, randomizes
the questions, and starts up a storyboard that runs for each
question. The randomization of the questions is an interesting
little bit of linq sitting in my view-model:
// randomize the order of the trivia questions
Random rand = new Random();
var randomizedList = from TriviaItem item in _repository.TriviaItems
orderby rand.Next()
select item;
The randomized questions are then loaded into an
ObservableCollection which is used for binding. Technically, since
I'm using a CurrentItem and MoveNext type approach, the observable
collection is redundant. I could have kept that as a
List<T>.
Automatic Navigation
The application is essentially a storyboard that repeats until
someone stops it. The storyboard steps are:
- Zoom the Question in from the left
- Show the image, if any (image slowly moves on a 3d plane
projection as well)
- Wait
- Show the countdown (5..4..3..2..1)
- Minimize the image, if any.
- Zoom the Answer in from the right and minimize the image
To move from question to question, I use a event handler in the
code-behind to catch the Storyboard completed event, move to the
next question, and restart the storyboard:
// storyboard completed event
void ShowQuestionAndAnswer_Completed(object sender, EventArgs e)
{
_viewModel.MoveNext();
ShowQuestionAndAnswer.Begin();
}
Manual Navigation
I wanted to provide the ability to skip questions, move back to
the previous question etc. Since I'm using Silverlight 3 for this
application, I couldn't use any built-in commanding. Instead, I
just forward keypress events to the MoveFirst, MoveNext and
MovePrevious methods. The code looks like this:
void TriviaQuestionAndAnswer_KeyUp(object sender, KeyEventArgs e)
{
// arrow keys move between questions
switch (e.Key)
{
case Key.Right:
case Key.Down:
case Key.PageDown:
ShowQuestionAndAnswer.Stop();
_viewModel.MoveNext();
ShowQuestionAndAnswer.Begin();
break;
case Key.Left:
case Key.Up:
case Key.PageUp:
ShowQuestionAndAnswer.Stop();
_viewModel.MovePrevious();
ShowQuestionAndAnswer.Begin();
break;
}
}
What Exactly is in the Code-Behind?
I love the viewmodel pattern, but I'm not going to go nuts
trying to have an empty code-behind unless I think that will
provide real value to the application. I'm not in the "your view
model must be empty" camp.
Like all of the rest of you, I had to write this application
under time constraints using a specific technology (Silverlight 3,
as SL4 wouldn't be on the PDC09 presentation machines). To that
end, I put UI logic into the code-behind when I felt it made sense.
Specifically:
- Keyboard event handling
- View model instance creation
- Data context setting (to the view model)
- Image failed event handler
- Storyboard Completed event handler
- A show function that starts the storyboard for the first
time.
With commanding in Silverlight 4, you can move some of those
things into the ViewModel, but overall, I think I made good choices
about what went in the code-behind vs. what went into the view
model. More importantly, I made informed and educated choices about
them. Much of the backlash against code-behind code comes because
many folks put code in there without learning about the
alternatives and trade-offs.
Fonts
PDC09 had its own signature font named "squared"; you could see
it all over the PDC artwork and the site. For that reason, the
PDC version of the application contained the embedded version of
the PDC font:
FontFamily="/PeteBrown.PdcTrivia;Component/Fonts/Fonts.zip#Squared"
The font itself was stored in the fonts folder in its native TTF
format. I used Expression Blend to do the UI and the font
embedding, so the Build Action for the font file is set to
BlendEmbeddedFont. That build action takes care of packaging,
subsetting (if you opt for it) and the overall embedding
process.
The MIX10 version uses standard fonts: Georgia and Arial.
Countdown
The countdown is actually composed of five different textblocks.
I did this so I could show the current one while transforming the
previous one and fading it out. For example, the "3" pops up while
the "4" is fading out and doing a scale transform to a zero
size.
There are other ways to accomplish this that may result in a few
fewer elements, but this was the easiest way to do it. It's pretty
flexible too since all the logic is in the storyboard itself.
Server
The server is our data repository. Other than some up-front
formatting, and the maintenance application, there's not much logic
there.
Maintenance App
For grins, I tried out asp.net 4 dynamic data. DD allowed me to
create a very simple maintenance application without worrying about
creating individual screens. That was the first time I used DD, and
my experience with it was pretty decent. Interestingly, it built on
several things I learned when working with WCF RIA
Services, the metadata approach being the most obvious.
Here's the main entity metadata, with some markup telling the UI
what types of controls to use for the editor page.
public class QuestionAndAnswerMetadata
{
[UIHint("FileUpload")]
public object QuestionImage { get; set; }
[ReadOnly(true)]
public object DateAdded { get; set; }
[ReadOnly(true)]
public object UserName { get; set; }
[StringLength(200)]
[UIHint("MultilineText")]
public object Question { get; set; }
[StringLength(200)]
[UIHint("MultilineText")]
public object Answer { get; set; }
}
The Silverlight client interacts with a simple WCF service on
the same server. I used a purpose-built entity, cross-compiled for
full .NET and Silverlight using the dual-project shared file
approach, for the messaging.
namespace PeteBrown.PdcTrivia.Entities
{
public class TriviaItem
{
public string Question { get; set; }
public string Answer { get; set; }
public string ImageUri { get; set; }
}
}
Let's talk about the service next.
The WCF Service
The WCF service pulls back all the trivia entries, as well as
the url for the images (see next section). It translates from the
dynamic data entity to the TriviaItem entity that is shared with
the Silverlight client. Here's the code:
[OperationContract]
public List<TriviaItem> GetTrivia()
{
List<TriviaItem> results = new List<TriviaItem>();
string imageStringFormat = HttpContext.Current.Request.Url.ToString().Replace("Services/TriviaService.svc", "") + "GetImage.aspx?id={0}";
PdcTriviaDataEntities context = new PdcTriviaDataEntities();
foreach (QuestionAndAnswer qa in context.QuestionsAndAnswers.ToList<QuestionAndAnswer>())
{
TriviaItem item = new TriviaItem();
item.Question = qa.Question;
item.Answer = qa.Answer;
if (qa.QuestionImage != null)
item.ImageUri = string.Format(imageStringFormat, qa.Id);
else
item.ImageUri = string.Empty;
results.Add(item);
}
return results;
}
Images: Hrefs to Blobs
I store the images in SQL Server as a blob field. This made it
easy to move the application around to different machines, as well
as use dynamic data to update the field.
Silverlight doesn't understand SQL Server, obviously, so I
created a .aspx page that will return the image. Another option
would have been a httmhandler, but a regular old .aspx page
requires the fewest configuration touch points.
The asp.net code to pull back an image from SQL Server and
return it to Silverlight looks like this:
protected void Page_Load(object sender, EventArgs e)
{
string idqs = Request.QueryString["id"];
if (string.IsNullOrEmpty(idqs))
{
Response.Write("Missing parameter 'id'.");
return;
}
int id;
if (!int.TryParse(idqs, out id))
{
Response.Write("Invalid id.");
return;
}
PdcTriviaDataEntities context = new PdcTriviaDataEntities();
Object entity = null;
IEnumerable<KeyValuePair<string, object>> keyValues = new KeyValuePair<string, object>[] { new KeyValuePair<string, object>("Id", id) };
if (context.TryGetObjectByKey(new EntityKey("PdcTriviaDataEntities.QuestionsAndAnswers", keyValues), out entity))
{
QuestionAndAnswer qa = (QuestionAndAnswer)entity;
if (qa.QuestionImage == null)
{
Response.Write("No image in that item.");
return;
}
// write the image
Response.Buffer = true;
Response.Charset = "";
Response.ContentType = "image/jpeg";
Response.BinaryWrite(qa.QuestionImage);
Response.Flush();
Response.End();
}
else
{
Response.Write("No such item.");
return;
}
}
That allowed me to have the entity contain only the Uri (which
is this page plus an id), keeping the initial Silverlight WCF
payload size small.
An interesting kludge is line 37, highlighted above. Setting the
content type to image/jpeg works for jpegs, but it turns out it
also works for the pngs I uploaded.
Hosting
The application doesn't require a lot of horsepower. Since I'm
using Silverlight on the client, the server needs only be able to
serve up the Silverlight app, the initial burst of data, and handle
any image requests. So, it's running on a workstation with Server
2008, IIS7, ASP.NET 4 RC, SQL Server 2008 and (for emergency
repairs) Visual Studio 2010 installed. As I recall, it's a dual
proc machine with 4gig RAM running the 64bit version of the OS.
That's my little "behind the scenes" for this app. If you see it
running at MIX, see how many of the trivia questions you can
answer.