Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

YeahTrivia: Creating a Trivia Server/Client with WPF and WCF

In this article I'll take you along for the ride as I attack the learning curve required to create a fun, interactive application using both the Microsoft Windows Presentation Foundation (WPF) and the Windows Communication Foundation (WCF). Afterwards, you'll have a flexible trivia client/server game ready to play by yourself or against friends, coworkers and your local know-it-all.  And in honor of Halloween, I've included three games of horror movie trivia.
The Bright Lights

Difficulty: Advanced
Time Required: 6-10 hours
Cost: Free
Software: Visual Studio Express, 2005, 2008 Beta 2 Microsoft .NET Framework 3.0
Hardware:
Download: Download Source

 

What can YeahTrivia do for me?

Glad you asked. This project started when I decided I wanted to dive headfirst into learning WPF and WCF, without hacking through countless Hello World examples.  And I wanted something fun that I could improve over time as my skills developed in these new technologies.  In this article I'll take you along for the ride as I attack the learning curve required to create a fun, interactive application using both the Microsoft Windows Presentation Foundation (WPF) and the Windows Communication Foundation (WCF). Afterwards, you'll have a flexible trivia client/server game ready to play by yourself or against friends, coworkers and your local know-it-all.  And in honor of Halloween, I've included three games of horror movie trivia.

In case you were wondering, the "YeahTrivia" name honors my new bride & her favorite prefix.  Think of it as YeeeeaaaaahhTrivia!

Basic Setup

This demo project was built using Visual Studio 2008 Beta 2, and is based on the Microsoft .NET Framework version 3.0.  The game is fed by a small windows app that acts as a game server, providing game selection, user registration, question loading, response times, chat messaging and scorekeeping.  The server loads a game by reading a predefined XML format, which we will look at a bit later.  This dynamic load lets anyone create their own flavor of trivia game, with any number of questions.

The server app exposes one WCF endpoint, a wsDualHttpBinding channel that supports duplex (two-way) communication between the server and any number of game clients.  Clients attach themselves to this endpoint and provide a callback address to the server.  The server saves an instance of this callback channel and uses each client's instance to contact the clients en masse during general game flow.  Enough talk, here's how to build one yourself.

Trivia Common and WCF Service Interfaces

The TriviaCommon project provides both the server and client projects access to shared objects.  To keep things simple the Common project only contains two classes:

Constants.cs defines the AnswerItem enumeration, which gives easy access to the five answer options (None, A, B, C, D), and the QuestionItem struct.  This structure is delivered to the client and contains question text, answer text, and the correct answer object of type AnswerItem.  Notice that objects being directly passed over the wire need to be marked with the [DataContract] attribute.

The second class, Interfaces.cs is where our WCF fun begins.  Take a look:

C#:
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;

namespace Trivia.Common
{
    #region Interfaces

    #region Service Interface

    /// <summary>
    /// Interface that defines Trivia service contract.
    /// </summary>
    [ServiceContract(CallbackContract = typeof(ITriviaCallback))]
    public interface ITrivia
    {
        [OperationContract(IsOneWay = true)]
        void RegisterUser(string userName);

        [OperationContract(IsOneWay = true)]
        void AcceptAnswer(string userName, string answerValue);

        [OperationContract(IsOneWay = true)]
        void ChatPublish(string userName, string message);
    }

    #endregion Service Interface

    #region Client Callback Interface

    /// <summary>
    /// Interface that defines Trivia client callback contract.
    /// </summary>
    public interface ITriviaCallback
    {        
        [OperationContract(IsOneWay = true)]
        void OnGameStart(int questionCount);

        [OperationContract(IsOneWay = true)]
        void OnGameEnd();

        [OperationContract(IsOneWay = true)]
        void OnUserRegistered(string userName);

        [OperationContract(IsOneWay = true)]
        void OnPublishChat(string userName, string chatMessage);

        [OperationContract(IsOneWay = true)]
        void OnQuestionLoad(int questionIndex, QuestionItem questionItem);

        [OperationContract(IsOneWay = true)]
        void OnQuestionEnd();

        [OperationContract(IsOneWay = true)]
        void OnQuestionStatusUpdate(Int64 questionTimeRemaining);

        [OperationContract(IsOneWay = true)]
        void OnScoreboardUpdate(Dictionary<string, Int64> scoreboard);

        [OperationContract(IsOneWay = true)]
        void OnGameServerStopped();
    }

    #endregion Client Callback Interface

    #endregion Interfaces
}


VB:

Imports System
Imports System.Collections.Generic
Imports System.ServiceModel
Imports System.Text

Namespace Trivia.Common

#Region "Interfaces"

#Region "Service Interface"
    
    ''' <summary>
    ''' Interface that defines Trivia service contract.
    ''' </summary>
    <ServiceContract(CallbackContract:=GetType(ITriviaCallback))> _
    Public Interface ITrivia
        <OperationContract(IsOneWay:=True)> _
        Sub RegisterUser(ByVal userName As String)

        <OperationContract(IsOneWay:=True)> _
        Sub AcceptAnswer(ByVal userName As String, ByVal answerValue As String)

        <OperationContract(IsOneWay:=True)> _
        Sub ChatPublish(ByVal userName As String, ByVal message As String)
    End Interface
        
#End Region

#Region "Client Callback Interface"

    ''' <summary>
    ''' Interface that defines Trivia client callback contract.
    ''' </summary>
    ''' <remarks></remarks>
    Public Interface ITriviaCallback
        <OperationContract(IsOneWay:=True)> _
      Sub OnGameStart(ByVal questionCount As Integer)

        <OperationContract(IsOneWay:=True)> _
        Sub OnGameEnd()

        <OperationContract(IsOneWay:=True)> _
        Sub OnUserRegistered(ByVal userName As String)

        <OperationContract(IsOneWay:=True)> _
        Sub OnPublishChat(ByVal userName As String, ByVal chatMessage As String)

        <OperationContract(IsOneWay:=True)> _
        Sub OnQuestionLoad(ByVal questionIndex As Integer, ByVal questionItem As QuestionItem)

        <OperationContract(IsOneWay:=True)> _
        Sub OnQuestionEnd()

        <OperationContract(IsOneWay:=True)> _
        Sub OnQuestionStatusUpdate(ByVal questionTimeRemaining As Long)

        <OperationContract(IsOneWay:=True)> _
        Sub OnScoreboardUpdate(ByVal scoreboard As Dictionary(Of String, Long))

        <OperationContract(IsOneWay:=True)> _
        Sub OnGameServerStopped()

    End Interface

#End Region

#End Region

End Namespace

A few items of interest here.  Notice we're importing the System.ServiceModel namespace.  This is the key assembly when using WCF, so don't forget to add a reference to your project.  The first interface, ITrivia, defines the operations the server is required to implement, which handle basic user actions: Register a team, accept an answer and accept a chat message.  Easy.

The second interface, cleverly named ITriviaCallback, lays out the callback contract a client must fulfill in order to play nicely with our server.  The operations listed here describe events the server will publish.  These aren't actual events, but the naming convention "OnX..." helps you remember that these are server published activities.

Notice that the [OperationContract] attribute notifies WCF that each method is part of a service contract.  The IsOneWay flag gives us "fire-and-forget" behavior (the app shouldn't wait around for a response).  So now that the common objects are ready, let's move on to...

Building the Trivia Server

The YeahTrivia Server is a simple utility app, but don't let the plain looks fool you; I thought about calling this the BrainStumper Pumper, but this is a family site.  This app lets a user (anyone acting as game admin) start the service, select the desired game, await team registrations, and then start the game.  If this were to become a larger scale project, I would make this an IIS hosted service that manages multiple games and provides more autonomy.  Until those sports bar royalty checks start rolling in, however, a utility app will do just fine.

The only form, GameServerDashboard, simply provides user interaction with the GameServer class.  On startup, the form inits an instance of GameServer and tells it to start the service.  It also loads all trivia question sets from the Games directory, and displays them in the list.

GameServer is the brain behind YeahTrivia. The attributes below set up the GameServer for action:

C#:

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant
        ,InstanceContextMode = InstanceContextMode.Single)]
    public class GameServer : ITrivia

 

VB:

<ServiceBehavior(ConcurrencyMode:=ConcurrencyMode.Reentrant, InstanceContextMode:=InstanceContextMode.Single)> _
    Public Class GameServer
        Implements ITrivia


ConcurrencyMode.Reentrant prevents deadlocks between the apps by stating that while this service is single-threaded, callbacks are allowed to invoke new operations.

InstanceContextMode.Single sets the service as a singleton, so that all messages are delivered to the same object instance.

These are advanced topics if you're new to WCF, so I'll include some links at the end that helped clarify some of this for me.  Let's keep moving forward by looking at a few activities of the game server:

Start the Service:  Passes in the configured base address as a Uri, inits and opens a new ServiceHost.

C#:

public void StartService()
{
   // Set base address and open service host
   Uri baseAddress = new Uri(ConfigurationManager.AppSettings[CONFIG_KEY_BASEURI]);
   _serviceHost = new ServiceHost(new GameServer(), baseAddress);
   _serviceHost.Open();
}

VB:

  Public Sub StartService()
     'Set base address and open service host
     Dim baseAddress As New Uri(ConfigurationManager.AppSettings(CONFIG_KEY_BASEURI))
     _serviceHost = New ServiceHost(New GameServer(), baseAddress)
     _serviceHost.Open()
  End Sub

 

Register a new team: Saves off the callback, adds it to the callback collection and scoreboard containers, notifies the user of a successful registration, and notifies all game clients of the new team.

C#:

 public void RegisterUser(string userName)
 {
     _currentCallback = OperationContext.Current.GetCallbackChannel<ITriviaCallback>();

     // Store callback
     if (!_callbacks.ContainsKey(userName))
     {
         _callbacks.Add(userName, _currentCallback);
         _scoreboard.Add(userName, 0);
         // Notify user of registration
         _currentCallback.OnUserRegistered(userName);
         // Notify clients of new user
         foreach (string userKey in _callbacks.Keys)
         {
            _currentCallback = _callbacks[userKey];
            _currentCallback.OnPublishChat(GAME_USERKEY, string.Format(FORMAT_USER_REGISTERED, userName));
         }                
     }
     else
     {
         // User already registered, throw exception
         throw new Exception(ERROR_USER_ALREADY_EXISTS);
     }
 }

VB:

Public Sub RegisterUser(ByVal userName As String) Implements ITrivia.RegisterUser
   _currentCallback = OperationContext.Current.GetCallbackChannel(Of ITriviaCallback)()

   'Store callback
   If Not _callbacks.ContainsKey(userName) Then
      _callbacks.Add(userName, _currentCallback)
      _scoreboard.Add(userName, 0)

      'Notify user of registration
      _currentCallback.OnUserRegistered(userName)

      'Notify clients of new user
      For Each userKey As String In _callbacks.Keys
         _currentCallback = _callbacks(userKey)
         _currentCallback.OnPublishChat(GAME_USERKEY, String.Format(FORMAT_USER_REGISTERED, userName))
      Next
   Else
      'User already registered, throw exception
      Throw New Exception(ERROR_USER_ALREADY_EXISTS)
   End If
End Sub

You can start to see how the server is able to handle incoming calls and process them back to the clients using the duplex channel.  I'll leave the rest of the server actions for you to examine on your own.

Building the Game Client (WCF)

Address, Binding, Contract.  These are the ABC's of WCF.  The client's app.config file handles connecting to the service and specifying the channel and contract used to interact with it.  Examine the client portion of the client .config:

<client>
   <endpoint address="http://localhost:8088/Trivia" binding="wsDualHttpBinding"
      bindingConfiguration="WSDualHttpBinding_ITrivia" contract="Trivia.Common.ITrivia"
      name="WSDualHttpBinding_ITrivia">
   </endpoint>
</client>

The address points to the exposed service endpoint.  The binding sets the channel as "wsDualHttpBinding" (our http duplex channel).  The bindingConfiguration attribute is an optional pointer to another group of configurations for the binding, and finally, the contract sets the interface with which our client will be interacting.

Next it's time to generate a proxy class.  Memorize this: Svcutil.exe is your friend.  The client needs a proxy class that it can use to interact with the service model.  The utility is a bit tricky at first, because of the wealth of optional flags, but stick with it, it's better than coding the proxy class by hand!  You can generate your client code by starting the service and running a command line similar to this:

svcutil /language:cs /out:ServerProxy.cs http://localhost:8088/Trivia

Add the generated class to the client project and you can begin to interact with the service.  The svcutil is available as part of the Windows SDK.  Check out the referenced site below for more information on this handy utility.

Building the Game Client (WPF)

Building the WPF portion of the client app was really a learning experience, and it will be the priority for me to improve in a later version.  Learning to program in XAML markup can seem daunting, and it requires a large paradigm shift on your part in order to grasp the new concepts.  Microsoft Expression is powerful tool for XAML work, but this time I coded all of the XAML by hand in order to fully see what was taking place inside.

A deep discussion of WPF is beyond the scope of this article, but take a look at the example code to get an idea of the layout.  An excellent book to start with is WPF Unleashed by Adam Nathan.  An easy read (for a programming book), and solid examples to get you rolling.

I've set up the client using a instance of the Window class, which is shown below:

<Window x:Class="Trivia.Client.GameClient"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="YeahTrivia" MinHeight="620" MinWidth="620" 
    WindowStartupLocation="CenterScreen" WindowState="Normal"  Style="{DynamicResource MainWindowStyle}"
    Background="{DynamicResource MainWindowBackgroundBrush}">

The window class sets up the outermost container of the app, and includes two default namespaces.  I've also set a few other properties here: a min height and width, window startup location & state, and two styling attributes set by dynamic resources.

Resources are a great way to further separate form & function, allowing a programmer to handle the behavior of an app, while a designer can work simultaneously on the look, all without stepping on too many toes.  As a 2.0 task for this project, I would remove all inline styling and move them into resource dictionaries.  A dictionary contains styles and control templates (a skin) that can be loaded & switched at runtime.

ResourceDictionaries are linked to your XAML in the App.Xaml file.  Once you've set up the reference, the example above show how to reference the current dictionary:  The Background and Style properties are set as dynamic resources by naming the Keys specified in the ResourceDictionary (DefaultResources.xaml):

<LinearGradientBrush x:Key="MainWindowBackgroundBrush" StartPoint=".45,.45" EndPoint=".55,.55" SpreadMethod="Reflect">
   <GradientStop Offset="0" Color="DodgerBlue"/>
   <GradientStop Offset=".8" Color="DarkSlateBlue"/>
</LinearGradientBrush>

<Style TargetType="{x:Type Window}" x:Key="MainWindowStyle">
   <Setter Property="FontFamily" Value="Trebuchet MS"/>
</Style>

Not quite as easy as CSS, but for a Forms project, it's a nice feature.  The sample ResourceDictionary includes a button control template for all of my buttons, based on Rob Eisenberg's excellent article (link below).

One other interesting feature to point out: Take a look at the code-behind class MarshalToUIThread method, which accepts a SendOrPostCallback parameter.  This is where the client is transferring actions from the sync thread back to the UI thread.  In each interface method implementation (OnX..), you can see how I wrap any UI-directed code and assign it to the postCallback field.  Passing this to the MarshalToUIThread method simply issues a SynchronizationContext.Post call, and the UI thread executes as expected.

Creating a New Trivia Game (XML)

New trivia games can be added at any time using the simple XML format shown here.  After installing the server application, simply drop the new .XML file into the Games directory ([Server Install Location]\Yeah Trivia Server\Games).  This demo comes pre-loaded with Geek Trivia, and a special Halloween Trivia version in case you're feeling festive.

<TriviaQuestionDataSet xmlns="http://tempuri.org/TriviaQuestionDataSet.xsd">
  <TriviaQuestion>
    <QuestionText>Name the highest grossing Monty Python creation.</QuestionText>
    <AnswerTextA>A) Holy Grail</AnswerTextA>
    <AnswerTextB>B) The Life of Brian</AnswerTextB>
    <AnswerTextC>C) Shrubbery</AnswerTextC>
    <AnswerTextD>D) The Meaning of Life</AnswerTextD>
    <CorrectAnswerKey>A</CorrectAnswerKey>
  </TriviaQuestion>
  <TriviaQuestion>
    <QuestionText>Ferris Bueller's best friend was _________.</QuestionText>
    <AnswerTextA>A) Sloane Peterson</AnswerTextA>
    <AnswerTextB>B) Edward R. Rooney</AnswerTextB>
    <AnswerTextC>C) Charlie Sheen</AnswerTextC>
    <AnswerTextD>D) Cameron Frye</AnswerTextD>
    <CorrectAnswerKey>D</CorrectAnswerKey>
  </TriviaQuestion>
</TriviaQuestionDataSet>

It's Time to Play YeahTrivia

Install both the Server and Client applications.  First, fire up the server, and then start your game client(s).  Once everyone has registered a team, select your game and hit "Start Game".    You're off...

Game Over

So there you have it.  We've seen how WCF manages communication between objects, extracting complicated service protocols away from your code.  And just a small subset of the WPF functionality has shown us how quickly a rich, XAML-based UI can come together.  And maybe, just maybe, you've learned why a CD is 74 minutes long... but I'm not telling.

I challenge you to improve on this initial version of YeahTrivia:  Crack open the XAML and add animations, images, video, music & more.  Create a self-maintaining server to run multiple games at once.  Add a High Score storage system.  Extend the WCF settings to enable other communication channels.  Add a new skin. Create new question files to share with us.  Whatever you do, have fun and go nuts.  YeahCoding4Fun.

Download source files

Download compiled demo (C# and VB)

About The Author

Steve Holstad works as a software consultant for Clarity Consulting in Chicago.  He blogs at The Bright Lights, and is available via email at sholstad@claritycon.com

 

References

wcf.netfx3.com/
wpf.netfx3.com/
Generating a client proxy class
ConcurrencyMode
SvcUtil.exe Tutorial
Rob Eisenberg's WPF control template

 

 

Special thanks to Bryan Dougherty for the WCF jumpstart, Kevin Marshall for the Halloween Trivia and Jay Garmon, creator of TechRepublic's Geek Trivia.

Tags:

Follow the Discussion

  • Gasper&#39;s Tech CornerGasper's Tech Corner

    If you want a good sample of building applications with WPF &amp; WCF give this a shot: http://blogs.msdn.com/coding4fun/archive/2007/10/29/5773166.asp...

  • jbmixedjbmixed

    cool, but the download links are broken!

  • Clint RutkasClint I'm a "developer"

    @jbmixed:  On it.

  • Clint RutkasClint I'm a "developer"

    @jbmixed: link fixed.  Thanks for the heads up.

  • Clint RutkasClint I'm a "developer"

    Thanks Nuno!

  • NunoNuno

    Found a workaround:

    In the client code:

    SyncContext = SynchronizationContext.Current;

                   InstanceContext instanceContext = new InstanceContext(this);

                   ServiceProxyInstance = new ServiceProxy(instanceContext);

    I added these lines:

                   WSDualHttpBinding binding = (WSDualHttpBinding)ServiceProxyInstance.Endpoint.Binding;

                   string clientCallbackAddress = binding.ClientBaseAddress.AbsoluteUri;

                   clientCallbackAddress += Guid.NewGuid().ToString();

                   binding.ClientBaseAddress = new Uri(clientCallbackAddress);

    And now it works!

    What do you think?

    Best regards,

    Nuno

  • NunoNuno

    If i try to run 2 clients in the same machine (and the server, naturally), the second client cannot register itsel, and i receive a framework error. Wasn't it supposed to work with as many clients as wished?

    Thanks in advance!

  • MichaelMichael

    Use of .vdproj file seems to keep source from opening with VS Express 2008. Any thoughts?

  • Johny79Johny79

    How can I get this project to run on another machine and have the server on another? I am getting an error that says:

    "Inconceivable! An error has occurred. The caller was not authenticated by the service"

    Help! I have a similar project like your and am running into issues.

    Thanks.

  • Clint RutkasClint I'm a "developer"

    @Johny79, contacting the author for advise on this.

  • Clint RutkasClint I'm a "developer"

    @Johny79:  This is the response I got:

    The project can run across multiple machines, as I tested this before publishing; judging from his comment he appears to have the same issue with a different project as well.  There are quite a few permissions/IIS/WCF security issues that may need to be checked within his environment to allow WCF communication to work.  if you receive more detail on issue we can think on it. I'd start with making sure a basic web service can be hosted and viewed from another machine, and then run Fiddler to determine if any errors are blocking communication.  I would also make sure the port number used is not being blocked along the way.

  • ksuvalkksuvalk

    Just came across this nice walk-thru and was wondering if anyone has "converted" this to Silverlight on the client side.  Just now learning it so not sure if it's a direct WPF port or not.  Thx!

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.