Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

YouGrade - Silverlight Multimedia Exam Suite

0.00/5 (No votes)
5 Jun 2010 2  
A multimedia exam suite built on Silverlight and Youtube

YouGrade

Contents

Introduction

This is my first Silverlight article in The Code Project. I only started using Silverlight in last March, and in April, I started working in a Silverlight project for my company. Although this article doesn't explore much of the eye-candy Silverlight features such as animations, my real focus here is to provide readers with a small, working proof-of-concept that explores the use of Silverlight as part of a multimedia exam suite. The initial idea was to create a simple exam suite, but it soon evolved to a multimedia exam suite, where you can assign a video to each question in the exam, thanks to YouTube's flexibility in providing a small player which can be embedded inside your HTML (and fortunately, inside your Silverlight applications).

YouTube is indeed a great video sharing tool, and YouGrade is taking advantage of YouTube's hosting capabilities by simply referencing a YouTube video code for each exam question. Notice that you can reference preexisting YouTube videos (such as those which I used in the "Basic French Exam" in this sample application), or you can create your own videos and upload them for your new exam.

You can have a quick overview of the application by taking a look at the YouTube video I uploaded, in the link below:

Acknowledgements

I'd like to thank some authors who gave me the basis on which I built this application and article.

First, and maybe the most important, is Katka Vaughan's Silverlight YouTube Jukebox, which gave me some insights about how to use HTML content hosting in Silverlight in a browser. Katka used DivElement's HtmlHost control, so that you can embed an HTML browser in a specific region of your Silverlight interface.

Then I got Colin Eberhardt's idea for element binding in Silverlight. As some of you may know, Silverlight 4 has element binding, but Silverlight 3 still lacked this feature, so Colin's idea was more than welcome for my article.

Another nice contribution was Patrick Cauldwell's MVVM: binding to Commands in Silverlight article, which allowed me to use the MVVM pattern to bind commands for some buttons in Silverlight 3.

Finally, I'd like to thank our prolific writers here in The Code Project, like Daniel Vaughan and Sacha Barber. Because of them, I was encouraged to start studying Silverlight and WPF seriously, and also to learn the MVVM pattern which I used in this article.

System Requirements

In order to get the application to work, you can download the following, if you don't have VS 2008 and Silverlight 3:

YouGrade Solution

Solution

Figure 1: Solution structure

The Visual Studio 2008 solution is made up of projects that collaborate with the Silverlight project, as we can see in the following table:

Project Description
YouGrade.Silverlight This is the Silverlight project itself.
YouGrade.Silverlight.Controls This project holds some of the custom controls I use in the project.
YouGrade.Silverlight.Web This project is the start up project that contains the application's entry page. Also, this web application holds the Silverlight-enabled WCF service that provides the Silverlight project with data services.
Yougrade.Silverlight.Core This Silverlight Class Library project just holds transfer objects (TOs) which are transferred between other projects.

Next, I split the solution into the two main parts of the article: Silverlight Project and Silverlight-Enabled WCF Service:

Silverlight Project

No doubt this is the main focus of this article. For the interface of this application, I could choose between many technology options, such as WPF, Windows Forms, ASP.NET, ASP.NET MVC, Silverlight, and so on. I finally decided for Silverlight for some reasons:

  • From the user's point of view, it's more convenient to access Web-based applications than to install and run Windows applications.
  • Many users don't have administrative privileges in their company machines, and installing the application might require additional technical support.
  • I'm sure ASP.NET or ASP.NET MVC would satisfy the needs of this application. But I also wanted a technology that provided users with a Rich Application Interface experience, without forcing the developer to know a lot of JavaScript and CSS.
  • And finally, I really, really wanted a strong motivation to start developing something with Silverlight...

The MVVM Pattern

Like many of WPF and Silverlight articles here in CodeProject, I also used the Model-View-ViewModel (MVVM) pattern. As Josh Smith stated, MVVM became the lingua franca for WPF developers. The same could be said about Silverlight developers, since the Silverlight framework is a subset of the WPF foundation.

For those of you who don't know MVVM yet, here's a little explanation: almost every developer knows what the "M" (Model) and "V" (View) mean. Model contains data and View presents the data. In most systems, the Model part maps database tables, and the View part shows those data to the user, via Windows Forms, plain HTML, WebForms, and in the case of WPF and Silverlight, this role is played by XAML files. So far, nothing new. But why do you need the ViewModel part? In fact, you are not forced to use View Models (VM) in Silverlight in any way, just like you are not forced to use the MVC pattern when you build an ASP.NET application. But there are people that think MVC is a nice pattern for ASP.NET, just like many people feel natural to use the MVVM pattern with XAML. In fact, it's so natural that MVVM was born while WPF was still under construction.

That being said, what's the difference between using or not using the ViewModel? Without the ViewModel, you would have something like the usual Windows Forms paradigm, where you put code on the MyForm.cs file to retrieve data from your business or data layer and then populate the fields in the form. In Silverlight, you could use the same approach and put code on your MyWindow.xaml.cs file to get data, and populate the XAML elements in the view, and also control the events like button clicks and listbox selection changes. The ViewModel approach, on the other hand, splits the presentation layer into View and ViewModel, so that the View doesn't retrieve data from the data source anymore. Instead, each View (that is, the XAML file) has its element properties bound to specific properties in the ViewModel class. Once these bindings are set up, the View elements and ViewModel properties stay synchronized, that is, each modification in one part automatically updates the other.

As we can see from Figure 2 below, the View doesn't access the data directly; instead, it relies on the bindings provided by its ViewModel counterpart. For example, a grid element named "gridProducts" in a View might have a binding to a ObservableCollection property named "Products" in the ViewModel. Any changes to the "Products" collection property would reflect in the gridProducts element on the View side. On the other hand, a Button element named "btnInsert" might have a binding to a Command property named "InsertCommand" in the ViewModel, so that any time the user clicks the button, the Insert() method on the ViewModel side is automatically invoked.

MVVM

Figure 2: Basic structure of the MVVM pattern

The Login View

As expected, the login view sits in front of everything else in our application. It simply authenticates the user and then blocks or allows access to the rest of our YouGrade application.

If you download and run the application, the only user you'll have is "John Doe", which has the login code and the password project. You may also create more users, if you wish, but you should do this by inserting users directly in the User table, because currently there is no appropriate user interface for this.

LoginView

Figure 3: The Login View

As you may find out, I didn't pay much attention to this view, but just because I rather focused mostly on the Exam View, which is the real core of the application. If this was a real application, I could use, for example, ASP.NET Membership, which provides powerful authentication features.

Now, back to the View: the following snippet (see the file: LoginView.xaml) shows that the txtLogin textbox has its Text property bound to the Login property of the ViewModel:

<TextBox x:Name="txtLogin" Grid.Row="1" Grid.Column="1" 
     Background="#FF202020" 
     Foreground="White" Margin="5" 
     CaretBrush="Yellow" TabIndex="0" 
     Text="{Binding Path=Login, Mode=TwoWay}" 
     TextChanged="txtLogin_TextChanged"  
     ToolTipService.ToolTip="Login is 'code' and password is 'project'"/>

On the other side, the LoginViewModel implements the Login property. Notice that this is 2-way binding, so any changes made to the text in the txtLogin element will reflect immediately in the Login property in the LoginViewModel. And any changes in the Login property will affect the text in the txtLogin element.

public string Login
{
    get { return login; }
    set { 
        login = value;
        OnPropertyChanged("Login");
        EnterCanExecute = login.Length > 0 && password.Length > 0;
        LoginEmpty = (login.Length == 0);
    }
}

It's also worth mentioning that the setter in the above property has three different notifications: one notification for the Login property itself, another one for the EnterCanExecute property, and the last one for the LoginEmpty property. You may wonder: why do I need all these notifications? The reason is that, instead of explicitly enabling/disabling an element's properties based on the login textbox, we want to take advantage of the MVVM pattern. The EnterCanExecute property enables/disables the Enter button, and the LoginEmpty property determines whether the yellow asterisk at the right side of the textbox should be visible or not.

As for the buttons, the following code shows that the XAML defines two bindings: one to define which command the button will execute, and another for determining whether the button should be enabled or disabled.

<Button x:Name="btnEnter" Grid.Column="0" 
    Content="Enter" Margin="5" 
    sc:ButtonService.Command="{Binding Path=EnterCommand}" 
    IsEnabled="{Binding Path=EnterCanExecute}" Click="btnEnter_Click" 
    ToolTipService.ToolTip="Login is 'Code' and password id 'Project'"/>

The Exam View

ExamView

Figure 4: The Exam View

As well explained by Katka Vaughan in her Silverlight YouTube Jukebox article, unfortunately, Silverlight lacked a control where you could host HTML content in your application, until the release of Silverlight 4 Beta (remember that this article deals with Silverlight 3 only). And even in Silverlight 4, you could only use WebBrowser or HtmlBrush while working on Silverlight Out Of Browser applications. But fortunately for us, DivElements company provides the HtmlHost control for free, and it works with in-browser applications, even with Silverlight 3.

The HtmlHost control is very simple to use. Notice in the Figure 5 below how we implemented the HtmlHost element, setting the SourceHtml property to a binding to the CurrentSourceHtml property on the ViewModel side:

<Border Name="HtmlHostContainer" Grid.Row="1" Grid.Column="1" Background="Transparent">
        <Border>
        <Border.Background>
            <ImageBrush ImageSource="/Images/ScreenBackground.png" 
               AlignmentX="Left" AlignmentY="Top" Opacity="100" Stretch="Uniform"/>
        </Border.Background>
        <divtools:HtmlHost SourceHtml="{Binding Path=CurrentSourceHtml}">
        </divtools:HtmlHost>
    </Border>
</Border>
Figure 5: How the HtmlHost control was used in the ExamView.xaml file

The HTML we want to put inside the HtmlHost control is just the JavaScript statements needed to run the Youtube Player. As seen in Figure 6 below, we used a helper class named QuestionConverter, to provide a set of functionalities, including the HTML necessary to run the Youtube Player with the correct YouTube video for the current question:

StringBuilder html = new StringBuilder();
html.AppendLine("   <SCRIPT type='text/javascript'> ");
html.AppendLine("       function onYouTubePlayerReady(playerId) {");
html.AppendLine("        alert('onYouTubePlayerReady!');");
html.AppendLine("       }");
html.AppendLine("   </SCRIPT>");

html.AppendLine("<object id=\"myytplayer\" width='322' " + 
                "height='270' bgcolor='#000000' disabled='true'>");
html.AppendLine(string.Format("  <param name='movie' " + 
                "value='http://www.youtube.com/v/{0}&rel=1&" + 
                "color1=0x2b405b&color2=0x6b8ab6&border=1'>", 
videoCode));
html.AppendLine("</param>");
html.AppendLine(string.Format("  <embed src='http://www.youtube.com/" + 
     "v/{0}&rel=0&color1=0x000000&color2=0x808080&border=0" + 
     "&autoplay=1&enablejsapi=1&playerapiid=ytplayer'", 
videoCode));
html.AppendLine("    type='application/x-shockwave-flash' " + 
                "wmode='transparent' bgcolor='#000000' " + 
                "width='315' height='265'></embed>");
html.AppendLine("</object>");
return html.ToString();
Figure 6: How we build the JavaScript tags to fill in the HtmlHost control

You can learn more from the Youtube Player API here.

HtmlHost

Figure 7: YouTube plug-in embedded in the HtmlHost control

There's one more important thing about the the Exam View: usually, the YouTube player shows advertisement links and links to other YouTube videos. In order to ensure the correct use of YouGrade, the user is not supposed to click such links and navigate away from the Silverlight application. I tried to use some "enabled=false" property on YouTube's JavaScript side, but I didn't find such a property. After some attempts and errors, fortunately, I ended up creating a HtmlHostContainer border element with a transparent background, which covers the HtmlHost that hosts the video player:

<Border Name="HtmlHostContainer" Grid.Row="1" Grid.Column="1" Background="Transparent">
    <Border>
        <Border.Background>
            <ImageBrush ImageSource="/Images/ScreenBackground.png" 
               AlignmentX="Left" AlignmentY="Top" Opacity="100" Stretch="Uniform"/>
        </Border.Background>
        <divtools:HtmlHost Name="htmlHost" IsHitTestVisible="False" 
             SourceHtml="{Binding Path=CurrentSourceHtml}">
        </divtools:HtmlHost>
    </Border>
</Border>

Custom Button Control

As a little XAML exercise, I decided to create a custom button with a nice look, which I called the "Green Button". It's a circular button which looks somewhat plastic, with a light effect on the top and a shadow effect on the bottom. At the center, you can choose which image you are using for the button. That's why the buttons on the ExamView are the same control. The only difference is the central image, as seen in Figure 9 below.

MVVM

Figure 9: Different images applied to the Custom Button control

While reading some books about Silverlight, some authors gave me advice to create a new Silverlight Class Library project and put the controls there.

In order to create the custom button, I had to first implement the button itself, which inherits from the Control class:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;

namespace YouGrade.Silverlight.Controls
{
    [TemplateVisualState(Name = "Normal", GroupName = "ViewStates")]
    [TemplateVisualState(Name = "Highlighted", GroupName = "ViewStates")]
    public class GreenButton : Button
    {
        public static readonly DependencyProperty ImageContentProperty =
        DependencyProperty.Register("ImageContent", typeof(object),
        typeof(GreenButton), null);

        public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string),
        typeof(GreenButton), null);

        public static readonly DependencyProperty IsHighlightedProperty =
        DependencyProperty.Register("IsHighlighted", 
                           typeof(bool), typeof(GreenButton), null);

        public GreenButton()
        {
            DefaultStyleKey = typeof(GreenButton);
            this.IsEnabledChanged += 
              new DependencyPropertyChangedEventHandler(
              GreenButton_IsEnabledChanged);
        }

        void GreenButton_IsEnabledChanged(object sender, 
                         DependencyPropertyChangedEventArgs e)
        {
            if (!(bool)e.NewValue)
            {
                this.IsHighlighted = false;
                ChangeVisualState(true);
                this.Opacity = 0.35;
            }
            else
            {
                this.Opacity = 1.00;
                VisualStateManager.GoToState(this, "Normal", true);
            }
        }

        public object ImageContent
        {
            get
            {
                return base.GetValue(ImageContentProperty);
            }
            set
            {
                base.SetValue(ImageContentProperty, value);
            }
        }

        public object Text
        {
            get
            {
                return base.GetValue(TextProperty);
            }
            set
            {
                base.SetValue(TextProperty, value);
            }
        }

        public bool IsHighlighted
        {
            get
            {
                return (bool)base.GetValue(IsHighlightedProperty);
            }
            set
            {
                base.SetValue(IsHighlightedProperty, value);
                ChangeVisualState(true);
            }
        }

        private void ChangeVisualState(bool useTransitions)
        {
            if (IsHighlighted)
            {
                VisualStateManager.GoToState(this, "Highlighted", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Normal", useTransitions);
            }
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            this.MouseEnter += new MouseEventHandler(GreenButton_MouseEnter);
            this.MouseLeave += new MouseEventHandler(GreenButton_MouseLeave);
            this.MouseMove += new MouseEventHandler(GreenButton_MouseMove);

            this.HorizontalContentAlignment = HorizontalAlignment.Center;
            this.ChangeVisualState(false);
        }

        void GreenButton_MouseMove(object sender, MouseEventArgs e)
        {
            this.IsHighlighted = true;
            ChangeVisualState(true);
        }

        void GreenButton_MouseEnter(object sender, MouseEventArgs e)
        {
            this.IsHighlighted = true;
            ChangeVisualState(true);
        }

        void GreenButton_MouseLeave(object sender, MouseEventArgs e)
        {
            this.IsHighlighted = false;
            ChangeVisualState(true);
        }

        private void NormalButton_Click(object sender, RoutedEventArgs e)
        {
            this.IsHighlighted = !this.IsHighlighted;
            ChangeVisualState(true);
        }
    }
}
Figure 10: Implementation of the GreenButton.cs class

Once we had the custom control class implemented, we needed to also create the template for it. So I created a file named "generic.xml" inside the Themes folder, to describe the templating for my new GreenButton control:

<!--GreenButton-->
<Style TargetType="local:GreenButton">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:GreenButton">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="ViewStates">

                            <VisualStateGroup.Transitions>
                                <VisualTransition To="Normal" 
                                          From="Highlighted" GeneratedDuration="0:0:0.1">
                                    <Storyboard>
                                        <ColorAnimation 
                                            Storyboard.TargetName="glowingGradientStop"
                                            Storyboard.TargetProperty="Color" To="#ff202020"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop"
                                            Storyboard.TargetProperty="Color" To="#ff008D00"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop21"
                                            Storyboard.TargetProperty="Color" To="#ff008D00"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop22"
                                            Storyboard.TargetProperty="Color" To="#ff008D00"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop23"
                                            Storyboard.TargetProperty="Color" To="#ff004000"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation Storyboard.TargetName="greenFill"
                                            Storyboard.TargetProperty="Color" To="#ff008D00"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <DoubleAnimation 
                                            Storyboard.TargetName="contentPresenter"
                                            Storyboard.TargetProperty="Opacity" To="1.00"
                                            Duration="0:0:0.1">
                                        </DoubleAnimation>
                                    </Storyboard>
                                </VisualTransition>
                                <VisualTransition To="Highlighted" 
                                          From="Normal" GeneratedDuration="0:0:0.1">
                                    <Storyboard>
                                        <ColorAnimation 
                                            Storyboard.TargetName="glowingGradientStop"
                                            Storyboard.TargetProperty="Color" To="Gold"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop"
                                            Storyboard.TargetProperty="Color" To="#ff56AB61"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop21"
                                            Storyboard.TargetProperty="Color" To="#ff56AB61"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop22"
                                            Storyboard.TargetProperty="Color" To="#ff56AB61"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation 
                                            Storyboard.TargetName="centralGradientStop23"
                                            Storyboard.TargetProperty="Color" To="#ff004000"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <ColorAnimation Storyboard.TargetName="greenFill"
                                            Storyboard.TargetProperty="Color" To="#ff56AB61"
                                            Duration="0:0:0.1">
                                        </ColorAnimation>
                                        <DoubleAnimation 
                                            Storyboard.TargetName="contentPresenter"
                                            Storyboard.TargetProperty="Opacity" To="0.80"
                                            Duration="0:0:0.1">
                                        </DoubleAnimation>
                                    </Storyboard>
                                </VisualTransition>
                            </VisualStateGroup.Transitions>

                            <VisualState x:Name="Normal">
                                <Storyboard>
                                    <ColorAnimation 
                                        Storyboard.TargetName="glowingGradientStop"
                                        Storyboard.TargetProperty="Color" To="#ff202020"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop"
                                        Storyboard.TargetProperty="Color" To="#ff008D00"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop21"
                                        Storyboard.TargetProperty="Color" To="#ff008D00"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop22"
                                        Storyboard.TargetProperty="Color" To="#ff008D00"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop23"
                                        Storyboard.TargetProperty="Color" To="#ff004000"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation Storyboard.TargetName="greenFill"
                                        Storyboard.TargetProperty="Color" To="#ff008D00"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <DoubleAnimation 
                                        Storyboard.TargetName="contentPresenter"
                                        Storyboard.TargetProperty="Opacity" To="1.00"
                                        Duration="0:0:0.1">
                                    </DoubleAnimation>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Highlighted">
                                <Storyboard>
                                    <ColorAnimation 
                                        Storyboard.TargetName="glowingGradientStop"
                                        Storyboard.TargetProperty="Color" To="Gold"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop"
                                        Storyboard.TargetProperty="Color" To="#ff56AB61"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop21"
                                        Storyboard.TargetProperty="Color" To="#ff56AB61"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop22"
                                        Storyboard.TargetProperty="Color" To="#ff56AB61"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation 
                                        Storyboard.TargetName="centralGradientStop23"
                                        Storyboard.TargetProperty="Color" To="#ff004000"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <ColorAnimation Storyboard.TargetName="greenFill"
                                        Storyboard.TargetProperty="Color" To="#ff56AB61"
                                        Duration="0:0:0.1">
                                    </ColorAnimation>
                                    <DoubleAnimation 
                                        Storyboard.TargetName="contentPresenter"
                                        Storyboard.TargetProperty="Opacity" To="0.80"
                                        Duration="0:0:0.1">
                                    </DoubleAnimation>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>

                    <Grid Width="64" Height="64" 
                              HorizontalAlignment="Center" VerticalAlignment="Center">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"/>
                            <RowDefinition Height="32"/>
                        </Grid.RowDefinitions>

                        <Grid Width="36" Height="36">
                            <Grid.Background>
                                <RadialGradientBrush 
                                          Center="0.5, 0.5" RadiusX="0.9" RadiusY="0.9">
                                    <GradientStopCollection>
                                        <GradientStop Offset="0.0" Color="#ff202020"/>
                                        <GradientStop x:Name="glowingGradientStop" 
                                               Offset="0.4" Color="#ff202020"/>
                                        <GradientStop Offset="0.6" Color="#ff202020"/>
                                        <GradientStop Offset="1.0" Color="#ff202020"/>
                                    </GradientStopCollection>
                                </RadialGradientBrush>
                            </Grid.Background>
                            <Ellipse Stroke="#ff008D00" StrokeThickness="1" Margin="5"/>
                            <Ellipse Stroke="#80A0C6A5" StrokeThickness="2" Margin="5.5"/>
                            <Ellipse Stroke="#ffA0C6A5" StrokeThickness="1" Margin="6"/>
                            <Ellipse Stroke="#8056AB61" StrokeThickness="2" Margin="6.5"/>
                            <Ellipse Margin="7">
                                <Ellipse.Fill>
                                    <SolidColorBrush x:Name="greenFill" Color="#ff008D00"/>
                                </Ellipse.Fill>
                            </Ellipse>
                            <Ellipse Margin="11,7,11,23">
                                <Ellipse.Fill>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                        <GradientStop Color="#ffffffff" Offset="0.00"/>
                                        <GradientStop x:Name="centralGradientStop" 
                                                 Color="#ff008D00" Offset="1.00"/>
                                    </LinearGradientBrush>
                                </Ellipse.Fill>
                            </Ellipse>
                            <Ellipse Margin="9,19,9,7">
                                <Ellipse.Fill>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                        <GradientStop x:Name="centralGradientStop21" 
                                              Color="#ff008D00" Offset="0.00"/>
                                        <GradientStop x:Name="centralGradientStop22" 
                                              Color="#ff008D00" Offset="0.50"/>
                                        <GradientStop x:Name="centralGradientStop23" 
                                              Color="#ff004000" Offset="1.00"/>
                                    </LinearGradientBrush>
                                </Ellipse.Fill>
                            </Ellipse>
                        </Grid>
                        <ContentPresenter Grid.Row="0" Margin="0,4,0,0" 
                           x:Name="contentPresenter" 
                           Content="{TemplateBinding ImageContent}" 
                           HorizontalAlignment="Center" 
                           VerticalAlignment="Center" Opacity="1.00"/>
                        <TextBlock Grid.Row="1" Foreground="White" 
                           HorizontalAlignment="Center" VerticalAlignment="Top" 
                           TextAlignment="Center" Canvas.Top="32" \
                           Text="{TemplateBinding Text}"/>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
Figure 11: Implementation of the template for the GreenButton control in the generic.xaml template file

Notice in the XAML above that the template has a ContentPresenter which points to the ImageContent template binding. This is how our custom control "binds" to the image we are using in each control instance.

Now, take a look at an example of how we declare an instance of GreenButton in the ExamView:

<lib:GreenButton x:Name="btnStart" Grid.Column="0" 
        Width="64" Height="64" HorizontalAlignment="Center" Text="Start" 
        sc:ButtonService.Command="{Binding Path=StartCommand}" 
        IsEnabled="{Binding Path=StartCanExecute}" Click="btnStart_Click">
    <lib:GreenButton.ImageContent>
        <Image x:Name="imgStart" Source="/Images/start.png" 
                Width="24" Height="24" Stretch="Fill"/>
    </lib:GreenButton.ImageContent>
</lib:GreenButton>
Figure 12: Example of how GreenButton is declared in the view

Clock/Timer Control

Our exam candidates will have a limited amount of time to finish the test. This is why we have a timer control.

The timer control is in fact a set of elements inside a Grid control. As you can see in Figure 13, the control is made up by a circular white background with two hands, for the minutes and for the seconds.

Clock

Figure 13: Clock control

The clock hands movement is implemented by two independent animations, from angles "-90" (12h) to "270" (6h). As we can see below, the clock hands are two thin rectangles rotating around one of their edges, each one with different speeds. As you should have figured out, the minutes hand takes 1 hour to complete the rotation, while the seconds hand takes 1 minute.

<Grid x:Name="grdClock" Grid.Column="6" Width="64" Height="64">
    <Grid.RowDefinitions>
        <RowDefinition Height="32"/>
        <RowDefinition Height="24"/>
    </Grid.RowDefinitions>
    <Ellipse Grid.Row="0" Width="32" Height="32" Stroke="DarkGray" 
       StrokeThickness="3" Fill="White" HorizontalAlignment="Center"/>
    <Rectangle Grid.Row="0" Width="10" Height="1" 
          Margin="12,0,0,0" Stroke="Black" StrokeThickness="1">
        <Rectangle.RenderTransform>
            <RotateTransform x:Name="rotMinutes" 
                     Angle="-90" CenterX="0" CenterY="0">
            </RotateTransform>
        </Rectangle.RenderTransform>
        <Rectangle.Triggers>
            <EventTrigger RoutedEvent="Rectangle.Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation x:Name="animMinutes" 
                            Storyboard.TargetName="rotMinutes" 
                            Storyboard.TargetProperty="Angle" From="-90" 
                            To="270" Duration="0:0:0" BeginTime="18:0:0" 
                            RepeatBehavior="Forever"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Rectangle.Triggers>
    </Rectangle>
    <Rectangle Grid.Row="0" Width="10" Height="1" 
            Margin="12,0,0,0" Stroke="Red" StrokeThickness="1">
        <Rectangle.RenderTransform>
            <RotateTransform x:Name="rotSeconds" 
                    Angle="-90" CenterX="0" CenterY="0">
            </RotateTransform>
        </Rectangle.RenderTransform>
        <Rectangle.Triggers>
            <EventTrigger RoutedEvent="Rectangle.Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation x:Name="animSeconds" 
                             Storyboard.TargetName="rotSeconds" 
                             Storyboard.TargetProperty="Angle" From="-90" 
                             To="270" Duration="0:0:0" BeginTime="18:0:0" 
                             RepeatBehavior="Forever"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Rectangle.Triggers>
    </Rectangle>
    <TextBlock Grid.Row="1" Foreground="White" 
      HorizontalAlignment="Center" Text="{Binding Path=Time}"/>
</Grid>
Figure 14: Implementation of the clock control

Notice that right below the clock elements, we have the numbers indicating the remaining time:

<TextBlock Grid.Row="1" Foreground="White" 
           HorizontalAlignment="Center" Text="{Binding Path=Time}"/>

One more thing: the exam timer can't start until the user clicks the Start button. Figure 15 shows how we set up the clock hands angle at the start of the test.

void examViewModel_TimerStarted(object sender, EventArgs e)
{
    rotMinutes.Angle = -90;
    animMinutes.BeginTime = new TimeSpan(0, 0, 0);
    animMinutes.From = -90;
    animMinutes.To = 270;
    animMinutes.Duration = new TimeSpan(1, 0, 0);
    rotSeconds.Angle = -90;
    animSeconds.BeginTime = new TimeSpan(0, 0, 0);
    animSeconds.From = -90;
    animSeconds.To = 270;
    animSeconds.Duration = new TimeSpan(0, 1, 0);
}
Figure 15: How we start the clock, in the ExamView.xaml.cs code-behind file

Silverlight-Enabled WCF Service

One of the most important points in the YouGrade application is the data access. We must be able to perform a number of data access operations, such as authenticating the user, getting exam definitions, and saving exam results to the database.

As many of you must know, Silverlight applications have no access to local resources, and limited access to network resources. The easiest way to do it is via WCF services (the other ways are HTTP communication and sockets communication). Fortunately, Visual Studio 2008 has a template named Silverlight-Enabled WCF Service which, as the name already says, facilitates the consumption of a WCF service by Silverlight applications, by automatically adding references and modifying the web.config file, for example.

Validating a User

The user is validated by taking a login and password and comparing them to the users in the database:

[OperationContract]
public User GetUser(string login, string password)
{
    using (YouGradeEntities1 db = new YouGradeEntities1())
    {
        var query = db.User
            .Where(e => e.Login.Equals(login, 
                   StringComparison.InvariantCultureIgnoreCase))
            .Where(e => e.Password.Equals(password, 
                   StringComparison.InvariantCultureIgnoreCase)
            );

        if (query.Any())
        {
            return query.AsQueryable().First();
        }
        else
        {
            return null;
        }
    }
}
Figure 16: Validating user

Getting Exam Definition

After validating the user, we must get the exam definition from the database. Notice that we are using Entity Framework for data access. Notice also that the Include("QuestionDef.Alternative") method tells the Entity Framework to return not only the exam definition, but also questions and alternatives associated to the exam definition.

[OperationContract]
public ExamDef GetExamDef()
{
    using (YouGradeEntities1 ctx = new YouGradeEntities1())
    {
        return ctx.ExamDef.Include("QuestionDef.Alternative").First();
    }
}
Figure 17: Getting exam definition

Saving Results to the Database

Saving the results to the database is a bit more complicated. Notice in the example below, that I used ExamTakeTO as a parameter, instead of the Exam entity. ExamTakeTO is a DTO (Data Transfer Object), that is, a POCO (Plain Old CLR object) that holds the data for the "exam take", that is, the data containing the user information along with the answers he or she chose for the questions.

[OperationContract]
public double SaveExamTake(ExamTakeTO examTakeTO)
{
    double grade = 0;
    try
    {
        using (YouGradeEntities1 ctx = new YouGradeEntities1())
        {
            User user = ctx.User.Where(e => e.Id == examTakeTO.UserId).First();
            ExamDef examDef = ctx.ExamDef.Where(e => e.Id == examTakeTO.ExamId).First();

            service.ExamTake newExamTake = service.ExamTake.CreateExamTake
                (
                    0,
                    examTakeTO.StartDateTime,
                    examTakeTO.Duration,
                    examTakeTO.Grade,
                    examTakeTO.Status.ToString()
                );

            newExamTake.User = user;
            newExamTake.ExamDef = examDef;

            ctx.AddToExamTake(newExamTake);

            ctx.SaveChanges();

            foreach (AnswerTO a in examTakeTO.Answers)
            {
                ExamTake examTake = ctx.ExamTake.Where(e => e.Id == newExamTake.Id).First();
                Alternative alternative = ctx.Alternative
                .Where(e => e.QuestionId == 
                       a.QuestionId).Where(e => 
                       e.Id == a.AlternativeId).First();
                Answer newAnswer = Answer
                .CreateAnswer(newExamTake.Id, a.QuestionId, a.AlternativeId, a.IsChecked);
                newAnswer.ExamTake = examTake;
                newAnswer.Alternative = alternative;
                ctx.AddToAnswer(newAnswer);
            }

            ctx.SaveChanges();

            foreach (QuestionDef q in ctx.QuestionDef)
            {
                var query = from qd in ctx.QuestionDef
                            join a in ctx.Answer on qd.Id equals a.QuestionId
                            join alt in ctx.Alternative on 
                            new { qId = a.QuestionId, aId = a.AlternativeId } 
                            equals new { qId = alt.QuestionId, aId = alt.Id }
                            where qd.Id == q.Id
                            where a.ExamTakeId == newExamTake.Id
                            select new { alt.Correct, a.IsChecked };

                bool correct = true;
                foreach (var v in query)
                {
                    if (v.Correct != v.IsChecked)
                    {
                        correct = false;
                        break;
                    }
                }
                grade += correct ? 1 : 0;
            }

            int examTakeId = examTakeTO.Id;
        }

        using (YouGradeEntities1 ctx = new YouGradeEntities1())
        {
            ExamTake et = ctx.ExamTake.First();
            string s = et.Status;
        }
        
        return grade;
    }
    catch (Exception exc)
    {
        string s = exc.ToString();
        throw;
    }
}
Figure 18: Saving the results to the database

Showing Result Report

After the user has finished the test, it's time to show the report.

The user will pass or fail the test, depending on whether the user has scored more or less points than the minimum required by the test (that is, the MinimumOfCorrectAnswers property of the Exam Definition entity).

As we can see in Figure 19 below, user John Doe failed the test because he scored only 4 points, while the minimum required was 10:

Failing the Test

Figure 19: Failing the test

Then poor John Doe studied a bit more and passed the test in another occasion, scoring 13 points:

Passing the Test

Figure 20: Passing the test

Entity Data Model

The entity data model (the edmx file) shown below matches exactly our database model. Below is a little explanation on each entity:

Entity Description
User Represents the user/student that is applying for the test.
ExamDef Represents the exam definition, containing name, definition, minimum of correct answers, and duration.
QuestionDef Represents the exam questions (along with the YouTube video IDs).
Alternative Represents the question alternatives.
ExamTake Represents the data for each user/student exam appliance.
Answer Represents the answers the user/student has chosen for the alternatives for each question.

Entity Data Model

Figure 21: The Entity Data Model

Wish List and Known Issues

There are still some issues with the application, and some desirable improvements which I hope I'll be correcting soon:

  • It would be great to have an exam definition editor. I'm planning to make one with WPF and publish it in a new article.
  • In the database model, the user is not linked to the exam definition in any way. It would be nice to have a schedule so that you could assign different exams to different users, control exam availability, validity periods, etc.
  • Sometimes the application doesn't properly stop the YouTube audio from playing while switching from one question to another.
  • I had some issues while passing the ExamDef entity from the Silvelight SaveExamTake method, and that's why I used transfer objects to pass data. Although it's not a critical problem, it would be nice and cleaner if I used only Entity Framework entities in those communications.

Final Considerations

I hope you have enjoyed this little application as much as the article. Feedbacks, please! Please tell me what you think of it, and please feel free to write about your complaints, suggestions, and advices.

History

  • 2010-05-18: First version.
  • 2010-05-28: Login View explained.
  • 2010-06-03: Exam View explained.
  • 2010-06-05: YouTube video added.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here