Use a DataBound Smiley Face for Super-Easy Data Comprehension

Posted on Updated on

Humans can understand a smile in a heartbeat, but they need to think to parse a number. You can deal with that by displaying your data with smiley faces. Result: you will put a smile on your users’ faces, making their data easy to understand. By doing so, you’ll reduce your app’s overall costs, because training costs will be lower. Also, your users will get their work done faster when they can understand their data rapidly. For some apps, that speed can make the difference between profit or loss, such as apps used in fast-paced financial markets. If your company is work a meritocracy, you will be rewarded when your reduce costs for your company!

Screen Shot Showing Data-bound SmileyFac
This app analyzes job skills, such as C# or WPF, from ads posted on Stack Overflow. The jobs that have been advertised the longest get a smile, so do jobs that require few other skills. Why? Because, when the job is filled, the ad is removed, so the jobs left, the oldest, are for skills that are rare or in demand (generally speaking). Likewise, some jobs require a whole set of skills, but skills that employers desperately want may be advertised all by themselves, because they are hard to find. Good news for us: if when employers seek hard-to-find skills, they have incentive to pay well and treat you better! So, whatever WPA, WFP and TCL (top 3 rows above) are, they might pay better because of supply and demand. Smile if you know either skill!
Three Smiles
Enlarged view makes it easier to see the differences between the smile sizes.

What We Will Use to Build Our Data-Bound Smiley Face

  1. A WPF UserControl
  2. WPF Geometry Drawing, including
    1. EllipseGeometry (to draw the eyes and the face)
    2. Geometry Path Markup Strings – to draw the smile
  3. DependencyProperty – we add one to our UserControl to expose our new property ‘SmilePct’
  4. DataBinding – I’ll share a real pain-saver that will help you bind UserControls in DataGrids
  5. DataGridTemplateColumn – to host our SmileyFace in our grid

Get Started: Build Your UserControl

Hopefully you already know how to add a UserControl to your project (right-click your project in Solution Explorer, choose, ‘Add’, then pick ‘UserControl’). We will build a user control that has an Image for its XAML (no grid or StackPanel needed). We will draw 3 different parts in our image:

  1. The Face
  2. The Smile (data-bound)
  3. The Eyes
Screen-shot: user control XAML
Skeleton of the UserControl, whose XAML consists of a single Image. By using a DrawingGroup, we can draw three parts in the same Image: 1) The yellow face, 2) The Smile, and 3) The Eyes. Each separate part will be an individual GeometryDrasing. What you see here is just the bare-bones; we will ad the rest immediately below.

XAML for the Face

For the face, we will use a simple circle (actually an ellipse, but you probably know that a circle is merely a special case of an ellipse) with a gradient background.

<!-- The yellow face -->
<GeometryDrawing>
    <GeometryDrawing.Geometry>
        <!-- Draw the circle, which will represent the face -->
        <EllipseGeometry RadiusX="12.5" RadiusY="12.5" Center="6.25, 6.25"  />
    </GeometryDrawing.Geometry>
    <GeometryDrawing.Brush>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="Gold" Offset="0"/>
            <GradientStop Color="Yellow" Offset="1"/>
        </LinearGradientBrush>
    </GeometryDrawing.Brush>
</GeometryDrawing>

To specify an ellipse, we describe the 1) size and shape using the RadiusX and RadiusY, plus 2) the position using the Center, which is described in Cartesian Coordinates (with the origin in the upper left corner of our Image). So, our face will be 25 pixels high (i.e. double the radius) and 25 pixels wide (double the RadiusX). Since RadiusX and RadiusY are equal, we will have a circle.

XAML for the Smiley

For the smile, we will use a string representation of the drawing to draw an ArcSegment. If you remember your geometry, or if you like to eat pies or pizza, you should already know that the ArcSegment is just the line represented by pizza crust. Basically a chunk of curve ripped out of a circle or, in the general case, an ellipse. We will build the geometry string in code; a sample geometry string looks like this: “M 0,10   A 10,25  0 0 0 12.5,10   Z”. I will explain how they work shortly; first, let’s look at the smiley XAML:

 <!-- The Smile, which is the only variable part-->
≶!-- The data binding will get the geometry from our code behind -->
 <GeometryDrawing Brush="White" Geometry="{Binding GeometryString}">
     <GeometryDrawing.Pen>
         <Pen Brush="RosyBrown" Thickness="1" />
     </GeometryDrawing.Pen>
 </GeometryDrawing>

It looks like not much is happening; that is because all the work is done via the Geometry property, which is bound to our code-behind. You can see the white brush, which is used for filling-in the teeth, and the pen (RosyBrown) which draws the lips. The shape of the lips is controlled via data binding, using a class property which I set in code. I will explain the Geometry string after explaining the XAML for the eyes. One further point: until now, I have not mentioned the data source I am binding to; in this case, I am binding to my own code-behind. I omitted that code until now (for clarity). To use your own code-behind for data binding, simply use the markup shown here to declare your UserControl:

<UserControl x:Class="CountJobTags.DataBoundSmiley"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
	     <!-- The following tells WFP to do all binding using our own code-behind as the source -->
             DataContext="{Binding RelativeSource={RelativeSource Self}}"
             d:DesignHeight="300" d:DesignWidth="300">

 XAML for the Eyes

For the eyes, we will draw two ellipses. They are similar to the face; their RadiusY is slightly larger than their RadiusX, to make them taller than wide, and they user different colors than the face.

<!-- The eyes-->
<GeometryDrawing Brush="DarkBlue">
    <GeometryDrawing.Pen>
        <Pen Brush="White" Thickness="1.5" />
    </GeometryDrawing.Pen>
    <GeometryDrawing.Geometry>
        <GeometryGroup>
            <!-- Draw 2 ellipses, each slightly taller than wide, for the eyes -->
            <!-- Left eye-->
            <EllipseGeometry RadiusX="2" RadiusY="2.5" Center="2,5" />
            <!-- Right eye-->
            <EllipseGeometry RadiusX="2" RadiusY="2.5" Center="10.5, 5" />
        </GeometryGroup>
    </GeometryDrawing.Geometry>
</GeometryDrawing>

 Now, for the UserControl Code-Behind

The code-behind has a simple structure: just two properties:

  1. The GeometryString property, which tells WPF how to draw the smile, and
  2. The SmilePct property, which allows the outside world to control the size of our grin. 100% → big smile, while 0% → straight face

You might have noticed from my screen shots that my face never frowns, it just varies from a straight line to a big smile. I did that because, for one reason, it is a little easier, and also because I don’t want any frowns in my apps!

DependencyProperties

OK, I only have two properties, but you should make them be special DependencyProperty instances, because that is what works best with UserControls. DependencyProperties are similar to INotifyPropertyChanged properties, but designed for special situations like UserControls, etc. Here’s a quick tip: Visual Studio will help you stub-out a DependencyProperty if you type the shortcut ‘propdp‘ and hit the tab key. For details, take a look at my Visual Studio Quick Tip on the Snippet Manager. Here’s what my two properties look like:

private static string NO_SMILE = "M 5,12.5 A 10,0 0 0 0 7.5, 12.5 Z";

public string GeometryString {
    get { return (string)GetValue(GeometryStringProperty); }
    set { SetValue(GeometryStringProperty, value); }
}

//Windows stores the property value (for GeometrySttring) on your behalf, so you don't
//declare any variable to hold the value
public static readonly DependencyProperty GeometryStringProperty =
    DependencyProperty.Register("GeometryString", typeof(string), 
	typeof(DataBoundSmiley), new PropertyMetadata(NO_SMILE));


public double SmilePct {
    get { return (double)GetValue(SmilePctProperty); }
    set { SetValue(SmilePctProperty, value); }
}

//Note the callback referenced below, 'OnSmileyPctChanged'
public static readonly DependencyProperty SmilePctProperty =
    DependencyProperty.Register("SmilePct", typeof(double), 
	typeof(DataBoundSmiley), new PropertyMetadata(0.0, OnSmileyPctChanged));	

 Where’s the Logic?

I deliberately skipped the complicated part, so that I could emphasize the fact that I only have two properties in my code-behind. All the work happens when the SmilePct is set to a new value, which causes the callback method ‘OnSmileyPctChanged‘ to be invoked. That is where we assign the value for our GeometryString. The geometry string will vary between these two extremes, which I will explain shortly:

  • Big Smile  = “M 0,10   A 10,25  0 0 0 12.5,10   Z”;
  • No Smile   = “M 5,12.5 A 10,0   0 0 0 7.5, 12.5 Z”;

Geometry Path String Explanation:

  • “M 0,10” means start the drawing at the coordinates (0,10). “M 5,12” means start the drawing at coordinates (5,12.5)
  • “A 10,25  0 0 0 12.5,10   Z” defines an ArcSegment, where
    • “A” means start the arc
    • 10,25” means RadiusX =10,RadiusY =25
    • 0 0 0” means: (first zero) rotate 0 degrees; (second zero) IsLargeArc = false; (third zero) means Clockwise = false
    • 12.5,10″ means finish the arc at coordinates (12.5,10)
    • Z” means close the arc, i.e. draw a line from the end point to the start point, which, for us, represents the top lip

Besides ArcSegments, you can draw LineSegments, BezierSegments and QuadraticBezierSegments. You can read more about them here: https://msdn.microsoft.com/en-us/library/ms752293(v=vs.110).aspx. So, basically, my method ‘OnSmileyPctChanged’ serves to build a string like either 1) the big smile string listed above, 2) the no smile string, or 3) some arc segment intermediate between them. I’ll list the code here, but leave it to you to figure-out the algebraic computations involved. Remember, my callback is invoked when the SmilePct property is set.

public static void OnSmileyPctChanged(object sender, DependencyPropertyChangedEventArgs e) {
    double theVal = (double)e.NewValue;
    //protect against bad data:
    if (theVal < 0)
        theVal = 0.0;
    else if (theVal > 1.0)
        theVal = 1.0;

    double xInit = 5 - (5.0 * theVal);
    double yInit = 12.5 - (2.5 * theVal);
    double pctHappy = (25 * theVal);
    double xTerm = 7.5 + (5.0 * theVal);
    double yTerm = 12.5 - (2.5 * theVal);
    DataBoundSmiley theInstance = (DataBoundSmiley)sender;
    theInstance.GeometryString = string.Format("M {0}, {1} A 10,{2} 0 0 0 {3},{4} Z",
                                               xInit, yInit, pctHappy, xTerm, yTerm);
}

Final notes on the code-behind:  the input parameter ‘sender‘  represents our UserControl; ‘e‘ is the event args, containing the NewValue (for SmilePct) and other info. Since the method is static (required by WPF), we need to set the value of the GeometryString on the specific instance of the control which is identified by the input parameter ‘sender’.

Code-Behind Summary

OK, at this point, I’ve shown you the code to create the UserControl, which is basically just a drawing with three ‘sub’ GeometryDrawing elements. We use ellipses for the face and eyes, and the smile is an ArcSegment which I bind to a DependencyProperty in my code-behind. The property ‘GeometryString’ is set in my method ‘OnSmileyPctChanged’; in that method,  I use some algebra to define the ArcSegment corresponding to a big smile for high value of SmileyPct. All told, the control consists of

  • 54 lines of XAML, and
  • 60 lines of code

Using the Data-Bound Smiley in a DataGrid

I won’t talk about DataGrid basics here, suffice it to say that I will add a template column to my DataGrid to host my smiley UserControl. Actually, I want the single column to display a smiley face and a numeric value, which is easy if I embed a grid inside my template. Here’s the XAML, which I have simplified to only display the grid and the template column:

<DataGrid AutoGenerateColumns="False" Name="grdCountsByTag" IsReadOnly="True"  >
    <DataGrid.Columns>
        <!-- Other columns omitted for brevity. Define a template column here: -->
	<DataGridTemplateColumn Header="Avg. Age In Hours" CanUserSort="True" SortMemberPath="AvgAgeInHours">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Grid>
                        <!-- Textblock holds the number -->
                        <TextBlock Text="{Binding AvgAgeInHours, StringFormat=N2}" />
                        <!--Now the smilley; note the data binding, which was tricky -->
                        <local:DataBoundSmiley Grid.Column="1" 
                                               SmilePct="{Binding DataContext.AvgJobAgePct, 
                                                          RelativeSource={RelativeSource AncestorType=DataGridRow}}" />
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition Width="auto" />
                        </Grid.ColumnDefinitions>
                    </Grid>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

Notes:

The prefix ‘local’ is an XMLNS definition which we place at the top of our form, like this:

<Window x:Class="CountJobTags.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:CountJobTags"
        Title="Analyze Job Skills" Height="750" Width="705"
        WindowStartupLocation="CenterScreen" >

The namespace declaration allows us to reference classes from our own project in the XAML. The TextBlock is bound to a separate (but related) column: AvgAgeInHours. The Smiley control is bound to a field named ‘AvgJobAgePct’. The first, AvgAgeInHours, varies between 0 and 672 (usually), while AvgJobAgePct varies between 0 and 1; the two are inversely proportional. The data binding expression also bears notice, as I spent almost as much time on this as the rest of the code. It was a major source of frustration! Some key points:

  • A normal control (such as my TextBlock) binds directly to the same data source as the grid
  • Maddeningly, custom controls do not, their data source is, AFAIK, themselves
  • Which is why I needed to use the RelativeSource syntax to find the data source for the Grid Row hosting our control, i.e.
    RelativeSource={RelativeSource AncestorType=DataGridRow}
  • The DataContex syntax (e.g. ‘DataContext.AvgJobAgePct’) is used to avoid binding to properties of the Grid Row, such as width/height, and instead goes for the data source used to display the row

Summary

The smiley control quickly conveys meaning to your users, making their lives easier. A UserControl is the best way to build a re-usable container to display your data – without it, you would not be able to display smileys for two separate columns. The UserControl I built uses an Image containing a GeometryGroup which, in turn, holds three GeometryDrawings (face, eyes and smile). The Smile is defined in my control’s code-behind as a DependencyProperty; I build it using the syntax for the WPF Path Markup. Specifically, I use it to draw an ArcSegment, whose curve and endpoints vary according to the SmilePct. If you want your users to easily comprehend your data, I recommend you use my data-bound smiley, because users recognize faces with little mental effort. To get download code, you can go to my open source project on CodePlex: http://visualizejobskills.codeplex.com/

Leave a comment