(40 votes)

Interactive Silverlight Voting Control

2 comments   /   posted by Ivan Dragoev on Dec 03, 2007

Update: Check the next part Creating a simple Voting control in Silverlight 2 which also runs on Silverlight 2

Update: We found an issue in the Voting control. When you click on the filled vote bar you actually give 2 votes because the mouse up event is handled by the two canvases that displays the vote. We've fix that so now you can download the fixed version.

Overview

We, at SilverlightShow.net, continue the series of articles focused on solving common tasks. In this article we will show you how to create a simple control that allows the user to vote by clicking on the answer.

The goal

Feedback from the site visitors is often needed. One way to get feedback is to allow the visitors to vote. We want to have a control, which can show the current results and at the same time can allow the user to give his/her vote for a certain answer by simply clicking on it.

The Solution

Data model

First we will start with making two classes - Vote and VoteOption.
The Vote class is used to store the descriptive text explaining for what the user is voting and also the list of options to vote.
public class Vote
{
    private string text;
 
    private Collection<VoteOption> items;
 
    public string Text
    {
        get
        {
            return this.text;
        }
        set
        {
            this.text = value;
        }
    }
 
    public Collection<VoteOption> Items
    {
        get
        {
            if ( this.items == null )
                this.items = new Collection<VoteOption>();
 
            return this.items;
        }
    }
 
    public int TotalVotes
    {
        get
        {
            int totalVotes = 0;
            foreach ( VoteOption voteItem in this.Items )
                totalVotes += voteItem.Votes;
 
            return totalVotes;
        }
    }
 
    public VoteOption AddItem( string text, int votes )
    {
        VoteOption voteItem = new VoteOption( this );
        voteItem.Text = text;
        voteItem.Votes = votes;
        this.Items.Add( voteItem );
        return voteItem;
    }
}
VoteItem – represents a single option related to a vote and includes text, number of votes and percentage based of all votes made.
public class VoteOption
{
    private readonly Vote vote;
    private string text;
    private int votes;
 
    public VoteOption( Vote vote )
    {
        this.vote = vote;
    }
 
    public Vote Vote
    {
        get
        {
            return this.vote;
        }
    }
 
    public string Text
   {
        get
        {
            return this.text;
        }
        set
        {
            this.text = value;
        }
    }
 
    public int Votes
    {
        get
        {
            return this.votes;
        }
        set
        {
            this.votes = value;
        }
    }
 
    public void VoteForIt()
    {
        this.Votes += 1;
    }
 
    public decimal VotesPercent
    {
        get
        {
            return Math.Round(( ( decimal )this.Votes / ( decimal )this.Vote.TotalVotes ), 4) * 100;
        }
    }
}

The controls

Having the simple data model allows us to continue with the user interface representation.

VoteOptionControl

Because one vote will have more than one vote options, we will create separate controls for representing every vote option. In Expression Blend you can see the control – the rootCanvas contains text block, where the vote option text will be displayed; resultCanvas, where the result will be displayed and percentValueRectangle that is used to show the current percentage based on all votes given for that option.
And the XAML:
<Canvas xmlns="http://schemas.microsoft.com/client/2007"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="625"
        Height="38"
        x:Name="rootCanvas"
        Cursor="Hand"
        >  
      <TextBlock Width="624" Height="16" Text="TextBlock" TextWrapping="Wrap" x:Name="tbTitle" Foreground="#FF000000" FontSize="12"/>
      <Canvas Width="624" Height="22" Canvas.Top="16" x:Name="resultCanvas" Opacity="0.7" Background="#FF536676">
            <Rectangle Width="361" Height="20" x:Name="percentValueRectangle" StrokeThickness="0">
                  <Rectangle.Fill>
                        <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
                              <GradientStop Color="#FFD03A0F" Offset="0"/>
                              <GradientStop Color="#FFF2FD29" Offset="0.99"/>
                        </LinearGradientBrush>
                  </Rectangle.Fill>
            </Rectangle>
      </Canvas>
</Canvas>
In VoteOptionContorl we make new properties for Width and Height, which calls base.Width and base.Height, but also calls private method InitializeLayout(). This method sets the positions and sizes of all elements depending on the size.
private void InitializeLayout()
{
this.tbTitle.SetValue( Canvas.TopProperty, 0 );
this.tbTitle.SetValue( Canvas.LeftProperty, 0 );
this.tbTitle.Width = this.Width;
this.tbTitle.Height = this.Height - this.voteResultCanvas.Height;
 
      this.voteResultCanvas.SetValue( Canvas.TopProperty, this.Height -     
            this.tbTitle.Height );
this.voteResultCanvas.SetValue( Canvas.LeftProperty, 0 );
this.voteResultCanvas.Width = this.Width;
 
this.percentValueRectangle.SetValue( Canvas.TopProperty, 1 );
this.percentValueRectangle.SetValue( Canvas.LeftProperty, 1 );
 
this.Refresh();
}
The other important method is the public method Refresh(). It is used to refresh the control – the option value text and also to display the vote result.
public void Refresh()
{
this.tbTitle.Text = string.Format( this.Formatting, this.VoteItem.Text,
            this.VoteItem.VotesPercent / 100, this.VoteItem.Votes );
 
decimal percent = this.VoteItem.VotesPercent;
if ( percent == 0 )
{
            this.percentValueRectangle.Visibility = Visibility.Collapsed;
}
else
{
            this.percentValueRectangle.Visibility = Visibility.Visible;
 
            if ( percent >= 100 )
                  this.percentValueRectangle.Width =
                        this.voteResultCanvas.Width;
            else
                  this.percentValueRectangle.Width = (
                        this.voteResultCanvas.Width / 100 ) *
                        ( double )percent;
}
}
 
Of course, to allow the site visitors to click on certain vote option we need to handle mouse button up event. We do that for tbTitle, voteResultCanvas and percentValueRectangle. In the event handler we raise our events Voting and Voted:
private void VoteOnMouseLeftButtonUp( object sender, MouseEventArgs e )
{
VoteEventArgs voteEventArgs = new VoteEventArgs( null );
this.OnVoting( voteEventArgs );
 
if ( voteEventArgs.Cancel )
            return;
 
this.VoteOption.VoteForIt();
 
this.OnVoted( voteEventArgs );
}
As you can see, our events work using custom event arguments VoteEventArgs:
public class VoteEventArgs : EventArgs
{
    private bool cancel = false;
    private readonly VoteOption voteItem;
 
    public VoteOption VoteItem
    {
        get
        {
            return this.voteItem;
        }
    }
    public bool Cancel
    {
        get
        {
            return this.cancel;
        }
        set
        {
            this.cancel = value;
        }
    }
 
    public VoteEventArgs( VoteOption voteItem )
    {
        this.voteItem = voteItem;
    }
}
Here the idea is to allow the consumer of the event to be able to perform some check and to cancel the voting if necessary. This might be used to check for example the IP address and to cancel the vote if it is from same IP. 
The last thing we will do is to give a hint to the user that he/she is allowed to vote. This will be made by changing the opacity of voteResultCanvas on MouseEnter and MouseLeave of tbTitle, voteResultCanvas and percentValueRectangle. Also we set the cursor for rootCanvas to be Hand.

VoteControl

VoteControl is simply a container for vote options and a header where the vote text is displayed.
<Canvas xmlns="http://schemas.microsoft.com/client/2007"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="640" Height="480"