Click here to Skip to main content
Click here to Skip to main content

Tagged as

Reputationator - CP Narcissists Rejoice! Part 4 of 4

, 30 Aug 2011
Rate this:
Please Sign up or sign in to vote.
Keep more detailed track of your Codeproject reputation points.
Chart showing daily points accumulation

Reputationator - Part 1 of 4 [^]
Reputationator - Part 2 of 4 [^]
Reputationator - Part 3 of 4 [^
Reputationator - Part 4 of 4 (this article)

Introduction

After posting the original three-part article series, someone asked for a WPF version. I saw no reason to do it because a WPF version would serve absolutely no purpose in terms of added capability or even fitness for purpose. Further, EVERYBODY here that's interested in Reputationator to begin with is a developer, and I assume that as such, EVERYBODY interested in seeing a WPF version can damn-well do it themselves. I still very much feel this way, and the ONLY reason I did this was to see how difficult it would be to do. As it turns out, it's not hard at all.

As you read the following, keep in mind that the interface code for the WPF project is completely different from the WinForms project on which it is based. BOTH projects now exist within the downloadabe zip file attached to this article series.

Finally, if you're expecting some whiz-bang WPF multimedia extravaganza, you're gonna be disappointed. I made no effort at all to make this anything more than as close a duplicate of the WinForms app as I could. However, if you want to embellish beyond basic appearance, you're a programmer, so be my guest. Keep in mind that if you need WPF help, I am NOT the person you want to seek out for help. Not because I'm too busy/don't care, but because I'm no expert on it (hell, I don't even really like WPF). If you do contribute, please let me know here, or post your own article. There is certainly a shortage of decent real-world examples of WPF apps, and yours will most certainly be welcome.

WinForms vs WPF - What Changed

I wanted to make it as easy on myself as possible (I'm not just an Outlaw Programmer, I'm a LAZY Outlaw Programmer). In this regard, I borrowed as much of the control names and functional code as I could from the WinForms app. Of course the UI, while similar in appearance, is completely different behind the scenes in regards to layout and interaction, and the differences are all implemented in the name of WPF.

Form Layout

As pretty much every .Net programmer already knows, WPF forms are laid out using XAML. This app is no different. However, I wanted to design the form in chunks, and toward that end, I created discrete UserControl objects that represent readily identifiable section of the form. These sections are the individual group boxes at the top of the form, the panel on the left side, and finally, the chart area. The purpose of these form sections is described in Part 3 of this article series, so I'm not going to go over it in detail again here. I'll simply list the control names:

  • 1) CtrlConfigPanel
  • 2) CtrlCurrentPointsPanel (*)
  • 3) CtrlEarnedForPeriodPanel (*)
  • 4) CtrlStatedGoalPanel
  • 5) CtrlYouVsLeaderPanel
  • 6) CtrlChartConfigPanel (*)

Controls not marked with a (*) in the list above don't have any code in them. The ones that ARE marked have a single line that facilitates data binding for the ListView controls contained therein.

Threading

In Winforms, when you want to update the UI from another thread, you do something like this:

    this.Invoke((MethodInvoker)delegate 
    {
        UpdateLeaderInfo(sender as ScrapeLeader, true);
    });

In WPF, that block of code looks like this:

    Dispatcher.Invoke(DispatcherPriority.Normal, (Action)delegate 
    {
        UpdateLeaderInfo(sender as ScrapeLeader, true);
    }

Like everything else in WPF, anything you don't do in the XAML takes a little more code in the .cs file. Since this article isn't about theory, if you want details about the WPF Dispatcher object, I recommend that you google it, or failing that, here's an explanation on MSDN:

MSDN - Dispatcher Class [^]

The Charts

WPF doesn't come with any kind of data visualization objects. For that functionality, you have to download a 3rd party library. The first place most people might go for chart support is the WPF Toolkit. That's where I went, mostly because we used the Silverlight Toolkit charting code at work, so I was somewhat familiar with the code and had some example code from which to draw from. That turned out to be a mistake. For two weeks, I fought with the template-based system used by that toolkit. The line and column charts were fairly simple, but the chart that gave me the heartburn was the Pie chart.

The pie chart shows data in a somewhat specialized way. As you know, each reputation category on CodeProject uses a specific color in the CodeProject static graph, and being the mostly agreeable sort that I am, wanted to mimic the color scheme that was in place. That's easy enough to facilitate on the line/column charts because they're used in alphabetical order, but with the pie chart, the data is presented in order by VALUE instead of by category name. Furthermore, it's not guaranteed that all of the categories will even have a place in the pie (any category with no points won't show up in the chart). All of this means that the pie chart required me to "color outside the lines" of the WPF Toolkit guys' definition of "expected usage".

My first problem was that I couldn't set the pie slice colors the way I wanted to. I tried a couple of different data binding things, but the pie slices either ended up transparent or the same color (orange) So, I finally ended up CHANGING the underlying data items so that they would each have a color property. Once I did that, I got the colors I wanted (because I could just bind to the property at that point), but the legend showed all orange (the default series color). That was the last straw. I really wanted to finish this app, and the WPF Toolkit was preventing me from accomplishing that goal (and pissing me off, to boot).

Another problem that I never put any effort into addressing (I was going to after I finished with the pie chart) was that the column chart was clipping the rendered data so that the data point at either end were not showing all the columns. I'm assuming that it was just a setting I missed somewhere, but we'll never kow because I kicked that crap code to the curb in favor of something MUCH better.

If you're sitting there wondering (like I was) why Microsoft didn't roll these data visulization classes into WPF4 along with all the rest of the WPF Toolkit controls, here's the reason - THEY'RE CRAP.

While searching (in vain) for a solution to my pie chart problems, I came accross what looked like a viable alternative to the Toolkit, and settled on the product from Visiblox (see the mini review at the end of this article). The net result was a REDUCTION in XAML and code behind of almost 500 lines of code (not to mention about 30 lines of content from this article describing some initial quirks of the Toolkit). I not only managed to completely refactor the chart code for the Visiblox library in less than a day, but I also simultaneously got the pie chart working excatly like I wanted it.

Tying It All Together

WARNING! You are about to have your sensibilities pushed to the very edge of acceptance.

I'm a lazy programmer. I'm more about getting the job done than adhering to some obscure paradigm regarding OOPacity. Toward that end, I did some stuff I normally don't do - I broke the rules regarding variable and object accessibility.

Remember above, where I told you about the various UserControl objects I created to make layout a little simpler? The net effect on the XAML is that instead of having a dizzing array of containers and children in one massive file, you end up with a handful of much smaller files with MUCH easier-to-follow layout. However, this kind of structure has a dark side (you're a programmer, you should have expected this).

It turns out that since each panel is its own UserControl, none of the child controls inside it are accessible from outside the control. I had a few ways to address this, but I chose the easy way - adding x:FieldModifier="public" to all of the controls that had some sort of interaction associated with them. This presents us with the often maligned theorum which states, "Write the code here, or write the code there, but no matter which one you chose, the code ultimately still needs to be written".

The net result of that decision was that I had to manually add all required event handlers for all necessary controls in the MainWindow class instead of having the designer do it for me. In my oh-so-humble opinion, the resulting convenience of having all of the even handling code in the MainWindow class was a reasonable tradeoff regarding the hassle of setting up the handlers in the code. In a larger (enterprise-level) application, I would probably be driven more by standard industry practices, but since this is an app I wrote more for myself than for anyone else, I was free to ignore standard practices and make it easier on myself.

I know - I'm a VERY naughty programmer.

Current points by day

New WPF-Specific Support Classes

Being a WPF app, I wanted to take at least a little advantage of its databinding features. There are two panels in the top part of the window that have no interaction associated with them, and that merely show data. Each of these panels feature a listview. In the Windows Forms app, I created each ListViewItem individually, but in the WPF app, this was not only unnecessary, it would have caused a LOT more code to be written (and lest you forget, I'm a lazy outlaw programmer). Data binding to the rescue.

First, I had to build the item objects and their respective collection classes. Because of WPF's databinding propensities, I had to use an ObservableCollection to hold the items. Once again, I created classes that inherits from the appropriate .Net collection class.

public class EarnedPointsCollection : ObservableCollection<EarnedPointsItem>
{
}

public class CurrentPointsCollection : ObservableCollection<CurrentPointsItem>
{
}

The items used in those collections are equally simple, and are comprised of a few properties to allow access to the data members. To make them WPF-ilicious, they inherit from INotifyPropertyChanged, although the ListViews are both "one-way" controls, so this wasn't completely necessary.

//////////////////////////////////////////////////////////////////////////////////////////
public class EarnedPointsItem : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

    private string m_periodName      = "";
    private string m_pointsValue     = "";
    private string m_pointsProjected = "";

    public string PeriodName
    {
        get { return m_periodName; }
        set { m_periodName = value; OnPropertyChanged("PeriodName"); }
    }

    public string PointsValue
    {
        get { return m_pointsValue; }
        set { m_pointsValue = value; OnPropertyChanged("PointsValue"); }
    }

    public string PointsProjected
    {
        get { return m_pointsProjected; }
        set { m_pointsProjected = value; OnPropertyChanged("PointsProjected"); }
    }
}

//////////////////////////////////////////////////////////////////////////////////////////
public class CurrentPointsItem : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

    private string m_categoryName = "";
    private string m_pointsValue  = "";
    private string m_pointsAvg    = "";

    public string CategoryName
    {
        get { return m_categoryName; }
        set { m_categoryName = value; OnPropertyChanged("CategoryName"); }
    }

    public string PointsValue
    {
        get { return m_pointsValue; }
        set { m_pointsValue = value; OnPropertyChanged("PointsValue"); }
    }

    public string PointsAvg
    {
        get { return m_pointsAvg; }
        set { m_pointsAvg = value; OnPropertyChanged("PointsAvg"); }
    }
}

The CheckListBox in the chart config panel required similar support, so I implemented the CLBCategoryItem and CLBCategoryList classes:

public class CLBCategoryList : ObservableCollection<CLBCategoryItem>
{
}

public class CLBCategoryItem : INotifyPropertyChanged
{
    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string name)
    {
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
    {
        handler(this, new PropertyChangedEventArgs(name));
    }
    }
    #endregion INotifyPropertyChanged

    private RepCategory m_enumValue = RepCategory.Unknown;
    private string      m_name      = "";
    private bool        m_isChecked = true;

    public string Name
    {
        get { return m_name; }
        set { m_name = value; OnPropertyChanged("Name"); }
    }

    public bool IsChecked
    {
        get { return m_isChecked; }
        set { m_isChecked = value; OnPropertyChanged("IsChecked"); }
    }

    public RepCategory EnumValue
    {
        get { return m_enumValue; }
        set { m_enumValue = value; OnPropertyChanged("EnumValue"); }
    }
}

WpfGlobals

To avoid passing parent references (or having to navigate the WPF method for finding parent controls, some of the data mambers that were located in the Form class (in the WinForms version) were put into the static WpfGlobals class. This allowed me to perpetuate my evil ways regarding child control accessibility outside of the UserControls that hosted them. Besides the (now relocated) RepScraper object, this class instantiates all of the new objects described earlier in this section. Here's the class, in its entirety:

//////////////////////////////////////////////////////////////////////////////////////
public static class WpfGlobals
{
    private static RepScraper              m_scraper                 = null;
    private static RepChartCollection      m_charts                  = null;
    private static CurrentPointsCollection m_currentPointsCollection = null;
    private static EarnedPointsCollection  m_earnedPointsCollection  = null;
    private static CLBCategoryList         m_clbCategories           = null;

    public static RepScraper Scraper 
    { 
        get         { return m_scraper;  }
        private set { m_scraper = value; }
    }

    public static RepChartCollection Charts  
    { 
        get         { return m_charts;  }
        private set { m_charts = value; }
    }

    public static CurrentPointsCollection CurrentPoints
    {
        get         { return m_currentPointsCollection;  }
        private set { m_currentPointsCollection = value; }
    }

    public static EarnedPointsCollection EarnedPoints
    {
        get         { return m_earnedPointsCollection;  }
        private set { m_earnedPointsCollection = value; }
    }

    public static CLBCategoryList CLBCategories
    {
        get         { return m_clbCategories;  }
        private set { m_clbCategories = value; }
    }

    //--------------------------------------------------------------------------------
    static WpfGlobals()
    {
        Scraper       = new RepScraper(false);
        Charts        = new RepChartCollection();
        CurrentPoints = new CurrentPointsCollection();
        EarnedPoints  = new EarnedPointsCollection();
        CLBCategories = new CLBCategoryList();
        foreach (RepCategory category in Enum.GetValues(typeof(RepCategory)))
        {
            if (category != RepCategory.Unknown)
            {
                CLBCategories.Add(new CLBCategoryItem() 
                { 
                    EnumValue = category, 
                    IsChecked = true, 
                    Name      = category.ToString() 
                });
            }
        }
    }
}

Changes to Chart Classes

Due to the fundamentally different ways the charts use their attached series, some changes to the copied WinForms code were necessary to create and maintain the charts in WPF. The chart classes have considerably less to do with regards to actually populating the chart with a series, and conversely, the base class is a lot busier (XAML requirements not withstanding). Still, there is less code inovlved than there is in the WinForms app. Essentially, there is just one method in each of the chart classes, and it's used to retrieve the specified data, and to call the appropriate base class methods to create the chart series objects. Each of the charts may need to manipulate the data so that it can be visualized according to the intended use of the chart.

ChartDailyChanges

This is the chart that shows the daily category accumulations in the form of a column chart. All we have to do is use the data as it was retrieved from the database, and either display the trend line or the data itself.

//--------------------------------------------------------------------------------
public override void PopulateChart(string comboTimePeriod, DateTime dateFrom, DateTime dateTo, DisplayCategories categories)
{
    this.ChartObj.Series.Clear();

    foreach(RepCategory category in categories)
    {
        List<RepItem> list = GetSeriesList(WpfGlobals.Scraper.Reputations, comboTimePeriod, category, dateFrom, dateTo);
        if (ShowTrendLine)
        {
            CalculateTrendLine(list, categories.Count);
        }
        else
        {
            MakeColumn(list, category.ToString(), "TimeScraped", "ChangeValue", Globals.CategoryColors[category]);
        }
    }
    FormatChartAxes();
}
Points accumulated since the beginning of the specified period

ChartAccumulated

This is the line chart that shows how many reputation points have been accumulated during the specified period. This means we have to do some additional calculations and build a proxy list to be used by the chart.

//--------------------------------------------------------------------------------
public override void PopulateChart(string comboTimePeriod, DateTime dateFrom, DateTime dateTo, DisplayCategories categories)
{
    this.ChartObj.Series.Clear();

    foreach (RepCategory category in categories)
    {
        List<RepItem> list = GetSeriesList(WpfGlobals.Scraper.Reputations, comboTimePeriod, category, dateFrom, dateTo);

        List<RepItem> list2 = new List<RepItem>();
        int startingValue = 0;
        for (int j = 0; j < list.Count; j++)
        {
            int zeroedValue = 0;
            RepItem item = list[j];

            if (j > 0)
            {
                zeroedValue    = item.Value - startingValue;
                m_highestValue = Math.Max(m_highestValue, item.Value - startingValue);
            }
            else
            {
                startingValue = item.Value;
            }

            RepItem item2 = item.Clone();
            item2.Value = zeroedValue;
            list2.Add(item2);
        }

        if (ShowTrendLine)
        {
            CalculateTrendLine(list2, categories.Count);
        }
        else
        {
            MakeDataLine(list2, category.ToString(), "TimeScraped", "Value", new SolidColorBrush(m_seriesColors[category]));
        }
    }
    FormatChartAxes();
}

ChartCurrentTrack

This is also a line chart, and it displays the actual point values for the selected categories within the specified time period. Sinc eit just shows the data that's there, it's PopulateChart method is identical to that used by ChartDailyChanges, so there's no reason to show you the code again.

ChartOverall

This is a pie chart showing the breakdown of the current value each category in terms of a percentage of the total points value. This chart is unique in that it actually sorts the data items according to their value, in ascending order. The only reason I do this is because that's the way I wanted to see it. Of course, this caused other issues with the chart which are described after the following code block.

//--------------------------------------------------------------------------------
public override void PopulateChart(string comboTimePeriod, DateTime dateFrom, DateTime dateTo, DisplayCategories categories)
{
    DateTime lastDate = WpfGlobals.Scraper.Reputations.Last().TimeScraped.Date;
    SetTitle(string.Format("as of {0}", lastDate.ToString("dd MMM yyyy")));

    List<RepItem> list = null;
    list = (from item in WpfGlobals.Scraper.Reputations
            where item.TimeScraped.Date == lastDate && categories.Contains(item.Category) && item.Category != RepCategory.Total
            select item).ToList();
    list.Sort(new GenericComparer<RepItem>("Value", GenericSortOrder.Ascending));

    MakePie(list, "Overall BreakDown", "Value");
}

The RepChart2 Class

Next to the main form, this is probably the most significantly changed class in the entire application when comparing it with the WinForms version. With the exception of the tooltip control templates, the charts are built entirely within the c# code (as opposed to XAML).

There are three types of charts used in the program. In keeping with the design of the WinForms application, each chart requires its own method for creating a series.

Column Series

The default DataSeries for a Visiblox chart amounts to a generic key/value pair, and as such, it doesn't have an ItemsSource property. For our needs, this isn't enough, but have no fear. If order to bind to OUR collection, all that needs to be done is to create a BindableDataSeries object, set its ItemsSource property to our data collection, and then set the ColumnSeries.DataSeries object to the newly created BindabledataSeries, and we're gold. Finally, we create a Style with the appropriate tooltip ControlTemplate, and we're done.

//--------------------------------------------------------------------------------
protected void MakeColumn(List<RepItem2> data, string seriesTitle, string xProperty, string yProperty, Brush colorBrush)
{
    if (this.ChartObj != null)
    {
        BindableDataSeries bSeries = new BindableDataSeries(seriesTitle);
        bSeries.ItemsSource   = data;
        bSeries.XValueBinding = new System.Windows.Data.Binding(xProperty);
        bSeries.YValueBinding = new System.Windows.Data.Binding(yProperty);
    
        ColumnSeries series   = new ColumnSeries();
        series.DataSeries     = bSeries;
        series.PointFill      = colorBrush;
        series.ToolTipEnabled = true;

        ControlTemplate toolTemplate = Application.Current.Resources["ChangeValueToolTipTemplate"] as ControlTemplate;
        if (toolTemplate != null)
        {
            series.ToolTipTemplate = toolTemplate;
        }
        this.ChartObj.Series.Add(series);
    }
}

Line Series

The LineSeries is almost identical to the ColumnSeries, with the primary exception being the stype of the data point on the chart. In the case of Reputationator, we want to see the points on everything but the trend lines.

//--------------------------------------------------------------------------------
protected void MakeDataLine(List<RepItem> data, string seriesTitle, string xProperty, string yProperty, Brush colorBrush)
{
    if (this.ChartObj != null)
    {
        BindableDataSeries bSeries = new BindableDataSeries(seriesTitle);
        bSeries.ItemsSource   = data;
        bSeries.XValueBinding = new System.Windows.Data.Binding(xProperty);
        bSeries.YValueBinding = new System.Windows.Data.Binding(yProperty);

        LineSeries series          = new LineSeries();
        series.DataSeries          = bSeries;
        series.LineStroke          = colorBrush;
        series.LineStrokeThickness = 1.5;
        series.ToolTipEnabled      = true;
        series.PointFill           = colorBrush;
        series.PointSize           = 5;
        series.ShowPoints          = (!this.ShowTrendLine);

        ControlTemplate toolTemplate = Application.Current.Resources["ValueToolTipTemplate"] as ControlTemplate;
        if (toolTemplate != null)
        {
            series.ToolTipTemplate = toolTemplate;
        }
        this.ChartObj.Series.Add(series);
    }
}

Pie Chart

The pie chart differs from the other charts in that it doesn't have a collection of series objects. Instead, the chart itself is the series. This means we have to do things a little differently, but not so uch that it's all that distracting. The quirkiest part of the whole thing was setting the colors for the various pie slices (remember, a given reputation category has a specific color assigned to it, and the data presented in the pie chart is sorted by point value instead of by category name. Furtrhermore, all categories my not even be represented (because the user doesn't have any points for it). All this adds up to a slightly more complicated setup.

//--------------------------------------------------------------------------------
protected void MakePie(List<RepItem> data, string seriesTitle, string yProperty)
{
    if (this.PieChartObj != null)
    {
        DisplayCategories categories = new DisplayCategories();
        foreach(RepItem item in data)
        {
            categories.Add(item.Category);
        }

        DataSeries<RepCategory, int> ds = new DataSeries<RepCategory,int>();
        this.PieChartObj.Palette.Clear();
        foreach(RepItem item in data)
        {
            ds.Add(item.GetPieDataPoint());
            this.PieChartObj.Palette.Add(MakePieSliceStyle(item.Category));
        }

        this.PieChartObj.DataSeries = ds;
        ControlTemplate toolTemplate = Application.Current.Resources["PieToolTipTemplate"] as ControlTemplate;
        if (toolTemplate != null)
        {
            this.PieChartObj.ToolTipTemplate = toolTemplate;
        }
        this.PieChartObj.ToolTipEnabled = true;
    }
}

When I started out with the WPF Toolkit, I was setting a LOT of styles in the code, and to keep the methods a little cleaner, I moved the code to it's own method. After switching to Visiblox, I no longer really needed the code (at least, not right now) but I kept it around, and eventually used it just for the pie chart, just so I could say there weren't any unreferenced methods in the code.

//--------------------------------------------------------------------------------
private Style MakePieSliceStyle(RepCategory category)
{
    Style style = new Style(typeof(PiePiece));
    SetStyleProperty(style, PiePiece.FillProperty, Globals.CategoryColors[category]);
    SetStyleProperty(style, PiePiece.StrokeProperty, Brushes.Black);
    SetStyleProperty(style, PiePiece.StrokeThicknessProperty, 1d);
    return style;
}

//--------------------------------------------------------------------------------
private void SetStyleProperty(Style style, DependencyProperty property, object value)
{
    Setter setter = new Setter(property, value);
    style.Setters.Add(setter);
}

Trend Lines

The line and column charts will call this method to create a trend line for each of the represented categories (if selected by the user).

//--------------------------------------------------------------------------------
protected void CalculateTrendLine(List<RepItem> data, int categoryCount)
{
    if (this.ChartObj != null)
    {
        if (this.ShowTrendLine)
        {
            LineSeries series = new LineSeries();
            List<RepItem> trendItems = new List<RepItem>();
            int pointCount = data.Count;

            double[] points = new double[pointCount];
            for (int i = 0; i < pointCount; i++)
            {
                points[i] = (this.TitlePrefix == "Daily Changes") ? data[i].ChangeValue : data[i].Value;
            }

            double a = 0;
            double b = 0;
            Globals.Regress(points, ref a, ref b);
            for (int i = 0; i < pointCount; i++)
            {
                trendItems.Add(data[i].Clone());
                double yield = (a + b) * i;
                if (this.TitlePrefix == "Accumulated By Day")
                {
                    trendItems[i].ChangeValue = Convert.ToInt32(yield);
                }
                else
                {
                    trendItems[i].Value = Convert.ToInt32(yield);
                }
            }

            MakeDataLine(trendItems, 
                         trendItems[0].Category.ToString(), 
                         "TimeScraped", 
                         (this.TitlePrefix == "Accumulated By Day") ? "ChangeValue" : "Value", 
                         Globals.CategoryColors[trendItems[0].Category]); 
        }
    }
}
Overall breakdown as of the current date

XAML Stuff

As much as I wanted to avoid doing anything in the XAML, I still had to create ControlTemplates for the tooltips used in each chart. The first two templates shown below only differ in the value property they're bound to, but the pie chart tooltip is considerably different since the there's no need to display the date associated with the pie slice item.

<local:TooltipValueConverter x:Key="TooltipValueFormatter" />
<local:TooltipDateConverter x:Key="TooltipDateFormatter" />
        
<ControlTemplate x:Key="ValueToolTipTemplate" TargetType="ToolTip">
    <Border BorderBrush="Gray" BorderThickness="1.5" Background="Azure" Padding="4" >
        <StackPanel Orientation="Vertical" >
            <TextBlock Text="{Binding Path=Category}" FontSize="12" />
            <TextBlock Text="{Binding Path=TimeScraped, 
                             Converter={StaticResource TooltipDateFormatter}}" 
                       FontSize="12"/>
            <TextBlock Text="{Binding Path=Value, 
                            Converter={StaticResource TooltipValueFormatter}}" 
                       FontWeight="Bold" FontSize="14" />
        </StackPanel>
    </Border>
</ControlTemplate>

<ControlTemplate x:Key="ChangeValueToolTipTemplate" TargetType="ToolTip">
    <Border BorderBrush="Gray" BorderThickness="1.5" Background="Azure" Padding="4" >
        <StackPanel Orientation="Vertical" >
            <TextBlock Text="{Binding Path=Category}" FontSize="12" />
            <TextBlock Text="{Binding Path=TimeScraped, 
                            Converter={StaticResource TooltipDateFormatter}}" 
                       FontSize="12"/>
            <TextBlock Text="{Binding Path=ChangeValue, 
                            Converter={StaticResource TooltipValueFormatter}}" 
                       FontWeight="Bold" FontSize="14" />
        </StackPanel>
    </Border>
</ControlTemplate>

<ControlTemplate x:Key="PieToolTipTemplate" TargetType="ToolTip">
    <Border BorderBrush="Gray" BorderThickness="1.5" Background="Azure" Padding="4" >
        <StackPanel Orientation="Vertical" >
            <TextBlock Text="{Binding Path=X}" FontSize="12" />
            <TextBlock Text="{Binding Path=Y, 
                            Converter={StaticResource TooltipValueFormatter}}" 
                       FontWeight="Bold" FontSize="14" />
        </StackPanel>
        </Border>
</ControlTemplate>

The original tooltip just showed the value represented by the data point, but given the nature of the charts, I also wanted to show the date and the category name associated with the value. Of course, this information needed formatting, so I implemented a couple of value converter classes.

//////////////////////////////////////////////////////////////////////////////////////
public class TooltipValueConverter : IValueConverter
{
    public object Convert(object value, Type TargetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string formatted = "UNK";
        int points;
        if (int.TryParse(value.ToString(), out points))
        {
            formatted = points.ToString();
        }
        return formatted;
    }

    public object ConvertBack(object value, Type TargetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }
}

//////////////////////////////////////////////////////////////////////////////////////
public class TooltipDateConverter : IValueConverter
{
    public object Convert(object value, Type TargetType, object parameter, System.Globalization.CultureInfo culture)
    {
        string formatted = "UNK";
        DateTime date;
        if (DateTime.TryParse(value.ToString(), out date))
        {
            formatted = date.ToString("dd MMM yyyy");
        }
        return formatted;
    }

    public object ConvertBack(object value, Type TargetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }
}

Visiblox - a Mini Review

I found Visiblox as a result of a google search, and after perusing their web site for a while, I decided to download their free library. The library is not a strictly templated library, which means you can do more in the code behind to customize the charts, which, given my aversion to all things "markup", I find particularly nice. To give you an idea of how little the chart actually relies on XAML, I created ALL of the charts in this application without using any markup, with the exception of the control templates for the tool tips. Rendering performance is great, too.

Kinda Klunky, but Not Altogether Unpleasant

When you download the Visiblox library, you get assemblies for use in Silverlight, WPF, and Windows Phone 7, along with complete documentation. While I found the library very easy to implement in my own admittedly minor application, there were some things that could be made better, if only by way of slightly improved documentation.

The documentation fully explains the library, and links to online examples. However, I think the docs could be bolstered by explaining some stuff that I had to discover on my own, mostly by looking at the example code on their website, and making some assumptions and deductions based on those examples.

  • You can't mess with the chart axis until AFTER you've added your series. I was already familiar with this aspect of WPF charts because the WPF Toolkit has the same characteristic.
  • If you want to bind your own collection instead of using their DataPointSeries class, you have to first create the appropriate series (ColumnSeries, LineSeries, etc), then create a BindableDataSeries, in which you set the ItemsSource property to your own collection, and then set the DataSeries property on the chart series to your BindableDSataSeries object. At that point, you can freely specify which of your object's properties to bind to. (I think they're addressing this in the upcoming v2.1)
  • There is no warning in the documentation that the pie chart is a completely different kind of chart with regards to series. Some of the stuff you did in the series of a line/column chart you have to do in the pie chart itself, such as tooltip styling, and messing with the palette.
  • If you want to change the colors for slices on the pie chart you MUST modify the Palette property (it's a collection of Style objects. The way I did it is by clearing the palette collection, and then just adding my own styles. Be aware that the styles are used in the order they are needed, and I haven't found any way to oevrride that functionality, so remember to add each style in the order you want to see it in the chart.

I think the documentation needs to be strengthened a bit, and the example charts should explore some of the less common usages (what I refer to as coloring outside the lines"), such as my requirement for specific colors in the pie chart in specific positions, but overall, the library is EXTREMELY easy to implement. I fought with the WPF Toolkit for two weeks, mostly trying to figure out the pie chart. After downloading Visiblox, I spent less than a day refactoring my existing code, and that INCLUDES time spent fixing my pie chart problems.

I had cause to contact technical support over an issue that I subsequently found the answer for almost immediately after sending the email. They responded in less than 24 hours. That's - well - outstanding. I even managed to exchange email a few times in the ensuing 12 hour period. Even more outstanding.

Pricing is (IMHO) too high if a single person wants to buy their chart library for hobby development, but the fact that they offer a free download of a slightly less capable version of their code (and that adds a watermark to all of the charts you render) significantly eases the pain. (Current pricing for the retail package is $850, and Mrs JSOP would freak out if I were to buy this myself.) HOWEVER, I have no problem at all recommending the purchase of this library in an environment with sufficient cash flow (corporate use). Here's a link to their site.

Visiblox

The End (Again)

I'm not the best advocate for WPF. I think it adds unnecessary complexity to development, and when you get right down to it, you get very little tangible results in exchange for that complexity. The tools provided by Microsoft are marginal (on a good day) and over-priced. Microsoft's lack of desire to add essential Expression Blend features into the IDE where they are best utilized by a developer is a major mental roadblock for me. Why should I be expected to have to a) spend money on, and b) run a second application (Blend) just so I can easily modify an OEM template? It's absurd, and I hate absurdity.

Anyway, I present you with a WPF app that you can play with, use, or toss into the trash, as you see fit.

History

  • 30 Aug 2011 - Posted updated article and a new download zip file with finished WPF code
  • 19 Aug 2011 - Original article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

John Simmons / outlaw programmer
Software Developer (Senior)
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.
 
My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
QuestionA tiny comment PinprotectorPete O'Hanlon30-Aug-11 5:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140827.1 | Last Updated 30 Aug 2011
Article Copyright 2011 by John Simmons / outlaw programmer
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid