Blog Stats
  • Posts - 298
  • Articles - 0
  • Comments - 3328
  • Trackbacks - 0


Saturday, March 01, 2008

My First WPF - Sales Leaderboard

So I've recently started to dig into the WPF bits of VS 2008 and decided to start with a small sample app.  Unlike most people that like to do the Hello World type apps, I prefer to practice on something that has actual utility in my business so I decided to build a leaderboard for our sales call center.  I'm going to go over how I accomplished this and hopefully help people get as excited as I am about WPF.  Excited is the proper word too, I've been reading Chris Anderson's book and it's been a great resource for explaining the hows and whys of the WPF architecture and frankly I am thrilled.  Microsoft has really taken a great step forward with helping designers and developers work together with VS 2008 and Expressions.  As you'll see in this sample, even with my limited graphical ability I'm able to do some interesting things with WPF.

So what I want to accomplish is to place a tab control on my form (so we can add new tabs for different views later), and we'll call it "sales".  The markup for this is pretty simple:

   1:      <StackPanel>
   2:          <Label Name="lblHeader" Content="Leader Board"/>
   3:          <TabControl>
   4:              <TabItem Header="Sales">
   5:                  <Grid>
   7:                  </Grid>
   8:              </TabItem>
   9:          </TabControl>
  10:      </StackPanel>

Next, inside the grid tag of the tab control, we're going to define a ListView control with a gridview, which will give us a grid-like appearance out of the box.  I'm going to put the name of the sales person, the number of calls they took, the number of sales they made, and their conversion rate (sales/calls).  The first pass looks like this:

   1:  <ListView Name="lstSalesLeaders">
   2:      <ListView.View>
   3:          <GridView>
   4:              <GridView.Columns>
   5:                  <GridViewColumn Width="125" Header="Name" DisplayMemberBinding="{Binding Path=Name}" />
   6:                  <GridViewColumn Header="Calls" DisplayMemberBinding="{Binding Path=NumberOfCalls}" />
   7:                  <GridViewColumn Header="Sales" DisplayMemberBinding="{Binding Path=NumberOfSales}" />
   8:                  <GridViewColumn Header="Conv" DisplayMemberBinding="{Binding Path=ConversionRate}" />
   9:              </GridView.Columns>
  10:          </GridView>
  11:      </ListView.View>
  12:  </ListView>

Notice the usage of the DisplayMemberBinding property.  WPF syntax likes to use the curly braces for bindings.  Path simply points to a property, data table column, etc on the collection I'm going to bind the listview to.  Now I tend to prefer working with Generic lists of objects rather than data tables, so I'm going to create a salesview class to hold the data I want:

   1:      class SalesView
   2:      {
   3:          public SalesView(string name, Int32 number_of_calls, Int32 number_of_sales)
   4:          {
   5:              Name = name;
   6:              NumberOfCalls = number_of_calls;
   7:              NumberOfSales = number_of_sales;
   8:          }
  10:          public string Name { get; set; }
  11:          public Int32 NumberOfCalls { get; set; }
  12:          public Int32 NumberOfSales { get; set; }
  14:          public string ConversionRate
  15:          {
  16:              get { return (NumberOfCalls == 0) ? "0.00%" : string.Format("{0:p}", Convert.ToDouble(NumberOfSales) / Convert.ToDouble(NumberOfCalls)); }
  17:          }
  18:      }

Now that this is complete, let's go ahead and bind the list in code.  We'll just create a few dummy objects at this point and then assign the resulting collection to the ItemsSource property of the list:

   1:          public Test()
   2:          {
   3:              InitializeComponent();
   4:              BindLeaderBoard();
   5:          }
   7:          private void BindLeaderBoard()
   8:          {
   9:              List<SalesView> sales = new List<SalesView>();
  11:              sales.Add(new SalesView("Salesperson 1", 10, 7));
  12:              sales.Add(new SalesView("Salesperson 2", 10, 4));
  13:              sales.Add(new SalesView("Salesperson 3", 10, 5));
  15:              lstSales.ItemsSource = sales;
  16:          }

So when we run this, we see we have a pretty plan looking grid, all black and white, very boring.  I'd like to spice it up a bit.  Let's say that every salesperson has a conversion goal of 45% sales and we want to change the forecolor of the grid row to be green if they meet or exceed the goal and red if they are below the goal.  WPF offers us some fun ways to style items in the way of data triggers.  The only problem I see with data triggers is that they don't seem to take a range, so we will add a boolean property to our SalesView class called IsMeetingConversionGoal.  Then all we need to do in our xaml is define a style tag in our listview and two data triggers, one for IsMeetingConversionGoal is true and one for false, the result looks like this:

   1:  <ListView.ItemContainerStyle>
   2:      <Style TargetType="{x:Type ListViewItem}">
   3:          <Style.Triggers>
   4:              <DataTrigger
   5:                    Binding="{Binding IsMeetingConversionGoal}"
   6:                    Value="True">
   7:                  <Setter Property="Foreground" Value="Green" />
   8:              </DataTrigger>
   9:              <DataTrigger
  10:                    Binding="{Binding IsMeetingConversionGoal}"
  11:                    Value="False">
  12:                  <Setter Property="Foreground" Value="Red" />
  13:              </DataTrigger>
  14:          </Style.Triggers>
  15:      </Style>
  16:  </ListView.ItemContainerStyle>

So now if we run the project, salesperson 2 is now red and the others are green!  Notice the use of the binding syntax yet again and the setter property.  You can set anything you want so you can get quite fancy with your displays pretty easily.  We're not through yet however!  Besides the data trigger, you can also trigger off a wide list of properties to update styles.  Lets get fancy and create a gradiant color fill on the row when we mouse over it.  WPF makes this task simple:

   1:              <Trigger Property="IsMouseOver" Value="true">
   2:                <Setter Property="Foreground" Value="DarkBlue" />
   3:                <Setter Property="Background">
   4:                  <Setter.Value>
   5:                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
   6:                      <GradientStop Color="#FFFFC704" Offset="0.986"/>
   7:                      <GradientStop Color="#FFF4E057" Offset="0.5"/>
   8:                      <GradientStop Color="#FFF4E057" Offset="0.51"/>
   9:                    </LinearGradientBrush>
  10:                  </Setter.Value>
  11:                </Setter>
  12:              </Trigger>

Run the project again, mouse over, and now you have a nice highlighted background with a gradiant.  I chose the gradiant just to show off that we're not bound to solid colors or importing gradiant background images like I always had to deal with in the web world. If you want to know more about brushes and gradiants, ask a designer, I'm not your man.

For my grand finale, I'm going to show you how to refresh this list every 60 seconds.  Of course this won't do much in the current project since we're not binding to a database, but the point to drive home is that the old Timer class doesn't play nice with WPF.  To handle timed events in WPF, you should use the new DispatcherTimer class located in System.Windows.Threading.  This class is really useful and easy to use as I'll demonstrate below:

   1:          private DispatcherTimer leaderboard_refresh_timer;
   3:          public Test()
   4:          {
   5:              InitializeComponent();
   6:              BindLeaderBoard();
   7:              InitializeTimer();
   8:          }
  10:          private void InitializeTimer()
  11:          {
  12:              leaderboard_refresh_timer = new DispatcherTimer(DispatcherPriority.Normal);
  13:              leaderboard_refresh_timer.Interval = TimeSpan.FromSeconds(60);
  14:              leaderboard_refresh_timer.Tick += new EventHandler(dispatcherTimer_Tick);
  16:              //Starts the downloading of the images 
  17:              leaderboard_refresh_timer.Start();
  18:          }
  20:          private void dispatcherTimer_Tick(object sender, EventArgs e)
  21:          {
  22:              BindLeaderBoard();
  23:          }
  26:          private void BindLeaderBoard()
  27:          {
  28:              List<SalesView> sales = new List<SalesView>();
  30:              sales.Add(new SalesView(10, "Salesperson 1", 10, 7, 0));
  31:              sales.Add(new SalesView(10, "Salesperson 2", 10, 4, 0));
  32:              sales.Add(new SalesView(10, "Salesperson 3", 10, 5, 0));
  34:              lstSales.ItemsSource = sales;
  35:              lblHeader.Content = "Leader Board (Last Update: " + DateTime.Now.ToShortTimeString() + ")";
  36:          }

So what we accomplished was initializing the DispatcherTimer, set the interval to 60 seconds and bound up the tick event.  Now the data doesn't change, so I decided to update the header label's content with a time stamp so you can see that the timer is in fact working and calling the bind method for the leader board.  So if we were connected to a live data source it would refresh the data accordingly.

I hope that this slightly more advanced "hello world" of WPF catches your imagination about the power and utility of the new framework.  I'm quite excited to start writing applications in it.



Copyright © Eric Wise