Click here to Skip to main content
15,879,326 members
Articles / Desktop Programming / Windows Forms

Reputationator - CP Narcissists Rejoice! Part 3 of 4

Rate me:
Please Sign up or sign in to vote.
4.85/5 (23 votes)
20 Aug 2011CPOL15 min read 43.3K   1.4K   24   24
Keep more detailed track of your Codeproject reputation points.
Download Reputationator.zip - 1.19 MB

screenshot_00.jpg

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

Introduction

This is the final part of this three-part article series, and what follows finally describes the application that utilizes the reputation data stored in the database. I hope it's not as boring as the first two parts. At least there are a number of pretty pictures to distract you from the endless minutial drone of code-speak.

I'd been working on this app in one form or another since June of 2010. My primary holdup was trying to decide how I wanted to store the data. My choices were an XML data file, SqlLite, or Sql Server Express. Almost immediately, I ruled out XML, and gave SQLLite a go. After a month on-and-off of trying to get it to work (it would compile but would eventually throw an exception of which I can no longer recall the details), I decided on Sql Server Express. My rationale is that pretty much everyone here has Visual Studio installed, and most likely Sql Server Express as well (even if they actually prefer another database). Later in this article, we'll talk about your options in that regard.

NOTE: - All screen shots below are based on about a months worth of data. It will take at least that long for you to start really enjoying what you see (let's face it, I can't just pull points values from the air for you). The word of the day in this regard is "patience".

Why I Wrote It

I care about my reputation points. There. I said it. You can say it, too, if you like. You're among friends here. I don't have any love at all for social networking stuff, and I spend more time on CodeProject than any other site that I frequent - by a VAST margin. You can probably tell that by looking at my reputation points. :)

Anyway, the reason I wrote this code was because the information provided by Codeproject simply isn't adequate. All we get is a static graph that shows our entire reputation history from our first day as a user, until today. It's impossible to tell precisely how far you've come since a given point in time, and there's no facility for looking at the data from different viewpoints.

Reputationator

There are several different graphs available that show your reputation posts and history (starting on the day that your started using this application). It also provides a daily average number of points earned for a given category, as well as some rudimentary predictive functionality based on the fore-mentioned daily averages.

The Charts

The charts use the same chart series color scheme as Codeproject.

Accumulated by Day - This chart is a column chart that shows the daily accumulation of points by category, as well as the total for each day, and data shown is for the specified time period.

Accumulated Since - This is a line chart that shows the number of points accumulated since the beginning of the specified time period.

Current Track - This is a line graph that shows your points history for the specified time period. Each points category is represented by its own line.

Overall Breakdown - This is a pie chart that shows each category and the percentage of your overall points value.

Time Periods

Reputationator will only show a maximum of 1 year's worth of data on the screen at a time. I did this because it seems to me that only the most recent year is really that important, and besides, it keeps the memory footprint low. The database will still contain data older than a year, and you can use existing program functionality to retrieve any block of data (no larger than 365 days).

Trend Lines

A Guided Tour

There's quite a bit happening on the Reputationator window, so I felt it wise to highlight and discuss each of the sections.

App Configuration

screenshot_00_config.jpg

This is probably going to be the least used area of the form, and honestly almost doesn't even deserve a place on the main form. However, being the lazy redneck that I am, I went with my first (bad) idea. From this panel, you can manage the Windows service (described in part 1 of this article series), specify your Codeproject User ID (doing this for anyone else's points is kinda - well - pointless), and perform a manual scrape (which is rarely if ever needed unless your way more of a narcissist than I am).

Current Points

screenshot_00_currpts.jpg

This section of the screen shows your current points value (as retrieved from the database), as well as the average daily accumulation for each category of points. These average values have a direct affect on the next panel.

Earned During Period

screenshot_00_projected.jpg

This panel shows the current values earned for the indicated periods, as well as the projected points you will have earned based on the current daily average of total points earned to date.

Stated Goal

screenshot_00_statedgoal.jpg

This panel allows you to state your points goal, and a date by which you wish to attain that goal. When you click the Recalculate button, the area to the right of the button tells you what the chances are that you will attain the stated goal.

Chart Configuration

screenshot_00_chartconfig.jpg

This panel allows you to determine which of the graphs you see, what categories are displayed in them, and for what time period. As you change the selected values in these controls, the chart will change and display its data according to what you've selected.

Chart Area

screenshot_00_chart.jpg

This is the chart area. The Y-axis refers to the number of points, and the X-Axis refers to the calendar date. Each series is represented in the legend on the right side of the chart.

The Code

If you're not really interested in the code, you can stop reading at this point, but since you have a few days to wait around before you can start showing any meaningful charts, you may as well keep reading. If you think an area deserves more explanation than I devoted to it, please say so, and I'll see what i can do about going into more detail. Keep in mind that I've been working on this code/article on and off for the better part of a year, so I'm getting kind bored with the whole thing (and neglecting my reputation points as a result - grin), so I may have inadvertently become somewaht terse in my explanation of the code. Have pity on me.

The RepChart Class

The RepChart class is a base class inherited by all of the outward-facing chart objects. You're correct if you assume that this class contains methods and properties that are common to most/all of the inheriting charts.

Properties and Data Members

First up is the color dictionary. It's purpose is to provide a simply method for assigning a color to the specified series. It's initialized in the InitSeriesColors() method (discussed later).

C#
public static Dictionary<RepCategory, Color> m_seriesColors = new Dictionary<RepCategory,Color>();

Then we setup a common font for chart titles.

C#
protected Font m_titleFont = new Font("Arial", 12, FontStyle.Bold | FontStyle.Italic, GraphicsUnit.Pixel);

Next, we have a MS Chart object (only makes sense, right?).

C#
public Chart ChartObj { get; private set; }

Then we have a custom event that's used to signify that there's no data to display.

C#
public event NoDataEventHandler NoDataEvent = delegate{};

And finally, some incidental stuff that makes our day a little easier.

C#
protected DateTime m_dateFrom;
protected DateTime m_dateTo;

public bool ShowTrendLine { get; set; }

The Constructor

We use the constructor to initialize the series colors, and the chart object.

C#
//--------------------------------------------------------------------------------
public RepChart(string name)
{
	this.ShowTrendLine         = false;

	InitSeriesColors();
	this.ChartObj              = new Chart();
	this.ChartObj.Name         = name;
	this.ChartObj.Dock         = System.Windows.Forms.DockStyle.Fill;
	this.ChartObj.Visible      = false;
	this.ChartObj.Tag          = this;
	this.ChartObj.Location     = new System.Drawing.Point(0, 0);
	this.ChartObj.TabIndex     = 1;
	this.ChartObj.Text         = name;
	this.ChartObj.Palette      = ChartColorPalette.BrightPastel;
    this.ChartObj.AntiAliasing = AntiAliasingStyles.All;
	this.ChartObj.Series.Clear();
	this.ChartObj.ChartAreas.Clear();
	this.ChartObj.Legends.Clear();
	ChartArea area = new ChartArea("ChartArea1");
	this.ChartObj.ChartAreas.Add(area);
	Legend legend = new Legend("Legend1");
	this.ChartObj.Legends.Add(legend);
}

protected void SetTitle(string period)

Sets the title for the chart object. By default, the chart title is the text that appears in the chart category combo box on the form. To make it more meaningful, this method is used to describe the represented time period, and is called by each of the charts.

C#
//--------------------------------------------------------------------------------
protected void SetTitle(string period)
{
    string title = this.ChartObj.Name;
    if (!string.IsNullOrEmpty(period))
    {
        title = string.Format("{0} {1}", this.ChartObj.Name, period);
    }
    if (this.ChartObj.Titles.Count == 0)
    {
        this.ChartObj.Titles.Add(new Title(title, Docking.Top, m_titleFont, Color.Black));
    }
    else
    {
        this.ChartObj.Titles[0].Text = title;
    }
}

protected virtual List<RepItem> GetSeriesList(...)

This method retrieves a list of reputation items based on the specified time period category, the reputation category, and the from/to dates. First up, are the sanity checks. I'll let the comments speak for themselves:

C#
//--------------------------------------------------------------------------------
protected virtual List<RepItem> GetSeriesList(RepItemCollection repItems, 
                                              string comboTimePeriod, 
                                              RepCategory category, 
                                              DateTime dateFrom, 
                                              DateTime dateTo)
{
    // no point in being here if there aren't any rep items
    if (repItems.Count <= 0)
    {
        throw new Exception(string.Format("No reputation items found. ({0} / {1})", 
		                                  comboTimePeriod, category.ToString()));
    }
    // there are no points associated with the Unknown reputaion category
    if (category == RepCategory.Unknown)
    {
        throw new Exception(string.Format("Invalid reputation cateory. ({0} / {1})", 
		                                  comboTimePeriod, category.ToString()));
    }
    // if this is for a specific time period, the dateFrom parameter must be 
    // valid (ticks > 0)
    if (comboTimePeriod == "Specified Period" && dateFrom.Ticks == 0)
    {
        throw new Exception(string.Format("Invalid start date (dateFrom) used for retrieving series data. ({0} / {1})", 
		                                  comboTimePeriod, category.ToString()));
    }
    // the "dateTo" must be set (ticks > 0) - we don't actually check to make 
    // sure it's >= the from date unless this is for a "Specified Period"
    if (dateTo.Ticks == 0)
    {
        throw new Exception(string.Format("Invalid end date (dateTo) used for retrieving series data. ({0} / {1})", 
		                                  comboTimePeriod, category.ToString()));
    }
    // so we can avoid a smart-ass programmer trying something like setting the 
    // dateTo parameter to a value that would result in a dateFrom being less 
    // than the lowest possible date (ticks = 0), we provide one last sanity 
    // check before proceeding, making sure that the dateFrom is really set to 
    // 0 ticks (but only if we're not doing  a "specified period"
    if (comboTimePeriod != "Specified Period" && dateFrom.Ticks > 0)
    {
        dateFrom.AddTicks(dateFrom.Ticks * -1);
    }

Next, we adjust the starting date based on the specified time period.

C#
// when we calculate the dateFrom, we do a Math.Max on the resulting ticks
// to mack sure we're not going to generate an invalid date
switch (comboTimePeriod)
{
    case "Current Week" :
        dateFrom = dateFrom.AddTicks(Math.Max(0, dateTo.WeekStartDate().Ticks));
        break;
    case "Current Month" :
        dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays((dateTo.Day - 1) * -1).Ticks)));
        break;
    case "Current Year" :
        dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays((dateTo.DayOfYear - 1) * -1).Ticks)));
        break;
    case "Last 7 days" :
        dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays(-7)).Ticks));
        break;
    case "Last 30 days" :
        dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays(-30)).Ticks));
        break;
    case "Last 365 days" :
        dateFrom = dateFrom.AddTicks(Math.Max(0, (dateTo.AddDays(-365)).Ticks));
        break;
    case "Specified Period" :
        if (dateTo < dateFrom)
        {
            throw new Exception(string.Format("End date (dateTo) cannot be ealier than start date (dateFrom). ({0} / {1})",
                                comboTimePeriod, category.ToString()));
        }
        break;
}

m_dateTo = dateTo;
m_dateFrom = dateFrom;

Finally, we extract and return the desired items from the master list of rep items.

C#
    // now that we have our from/to dates we can retrieve/return the specified items
    List<RepItem> list = null;
    list = (from item in repItems 
            where ((item.Category == category) && (item.TimeScraped.Between(dateFrom, dateTo, true)))
            select item).ToList();
    return list;
}

protected virtual int NormalizeMaxY(int maxY)

This method determines the highest value to be represented on the Y-axis of the chart. It generally works pretty good, but I feel like it could use some tweaking.

C#
//--------------------------------------------------------------------------------
protected virtual int NormalizeMaxY(int maxY)
{
    int thousands = (int)Math.Ceiling(maxY / 1000d);
    if (thousands > 100)
    {
        thousands += (50 -  (thousands % 50));
    }
    else if (thousands > 10)
    {
        thousands += (5 - (thousands % 5));
    }
    maxY =  1000 * thousands; 
    return maxY;
}

protected virtual int NormalizeInterval(int maxY)

This method determines the Y-Axis interval based on the max possible value on the Y-Axis. Again, generally speaking, it works pretty well, and could probably use some tweaking.

C#
//--------------------------------------------------------------------------------
protected virtual int NormalizeInterval(int maxY)
{
    int interval = 0;
    if (maxY > 100000)
    {
        interval = 50000;
    }
    else if (maxY > 10000)
    {
        interval = 10000;
    }
    else if (maxY > 1000)
    {
        interval = 1000;
    }
    else 
    {
        interval = 100;
    }
    return interval;
}

protected void CalculateTrendLine(Series series, int categoryCount)

This method calculates the trend based on the specified parent series, and creates a series for the chart.

C#
//--------------------------------------------------------------------------------
protected void CalculateTrendLine(Series series, int categoryCount)
{
    if (this.ShowTrendLine)
    {
        int pointCount   = series.Points.Count;
        Series trend     = new Series("Trend", pointCount);
        trend.ChartArea  = this.ChartObj.ChartAreas[0].Name;
        trend.ChartType  = SeriesChartType.Line;
        trend.XValueType = ChartValueType.DateTime;
        trend.YValueType = ChartValueType.Double;
        trend.Color      = (categoryCount > 1) ? series.Color : Color.Black;

        double[] points = new double[pointCount];
        for (int i = 0; i < series.Points.Count; i++)
        {
            DataPoint point = series.Points[i];
            points[i] = point.YValues[0];
        }

        double a = 0;
        double b = 0;
        Globals.Regress(points, ref a, ref b);
        for (int i = 0; i < pointCount; i++)
        {
            double yield = a + b * i;
            trend.Points.AddXY(series.Points[i].XValue, yield);
        }

        trend.Name = string.Format("{0} Trend",series.Name);
        this.ChartObj.Series.Add(trend);
    }
}

The RepChartCollection Object

When I'm using generic Lists, I like to create a new class that inherits from List. It eases typing, especially for articles here on CodeProject, because you don't have to worry about pointy brackets and underlying object types. It is infrequent that I actually need to put code into these classes, but it's pretty handy to have an object you can put collection-specific code into in an effort to keep the outward-facing code a little more pristine. This collection object only has a couple of methods.

The Constructor

For this application, we're not dynamically creating charts, or removing them from the collection. For this reason, the constructor creates all of the charts the app uses, and adds them to the collection.

C#
//--------------------------------------------------------------------------------
public RepChartCollection()
{
    ChartDailyChanges dcChart = new ChartDailyChanges("Accumulated By Day");
    ChartCurrentTrack ctChart = new ChartCurrentTrack("Current Track");
    ChartOverall      ovChart = new ChartOverall     ("Overall Breakdown");
    ChartAccumulated  acChart = new ChartAccumulated ("Accumulated Since");
    this.Add(dcChart);
    this.Add(ctChart);
    this.Add(ovChart);
    this.Add(acChart);
}

public RepChart GetChartByName(string name)

This method retrieves the named chart (the names are used to create the charts in this class' constructor).

C#
//--------------------------------------------------------------------------------
public RepChart GetChartByName(string name)
{
    RepChart chart = (from item in this 
                      where item.ChartObj.Name == name
                      select item).FirstOrDefault();
    return chart;
}

public void ShowChart(string name)

This method makes the named chart visible, and hides all of the others.

C#
//--------------------------------------------------------------------------------
public void ShowChart(string name)
{
    foreach(RepChart chart in this)
    {
        chart.ChartObj.Visible = (chart.ChartObj.Name == name);
    }
}

The Chart Objects

I'll be limiting the discussion to just one of the chart objects because they're all pretty much the same. The chart object we'll be discussing is the ChartAccumulated. It's a line chart that represents all points gaind since the beginning of the specified time period. All of the other charts are virtually identical (except for the pie chart) in form and function.

public override void PopulateChart(...)

This method overrides the base class and is responsible for populating the chart with one series per selected reputation category, and configuring the axis. Each time the user selects something in the chart configuration panel, this method is eventually called so that we get a whole new subset of data.

C#
//--------------------------------------------------------------------------------
public override void PopulateChart(ReputationLib.RepScraper scraper, string comboTimePeriod, DateTime dateFrom, DateTime dateTo, DisplayCategories categories)
{
    m_highestValue = 0;
    this.ChartObj.Series.Clear();
    DateTime now = DateTime.Now.Date;

    foreach (RepCategory category in categories)
    {
        if (category == RepCategory.Unknown)
        {
            continue;
        }
        List<RepItem> list = GetSeriesList(scraper.Reputations, comboTimePeriod, category, (comboTimePeriod == "Specified Period")?dateFrom:new DateTime(0), dateTo);

        Series series = CreateSeries(category, list);
        if (this.ShowTrendLine)
        {
            if (list.Count > 1)
            {
                CalculateTrendLine(series, categories.Count);
            }
        }
        else
        {
            this.ChartObj.Series[category.ToString()] = series;
        }
    }

    SetTitle(string.Format("{0}", m_dateFrom.ToString("dd MMM yyyy")));

    int maxY     = NormalizeMaxY(m_highestValue);
    int interval = NormalizeInterval(maxY);

    this.ChartObj.ChartAreas["ChartArea1"].AxisY.Minimum             = 0; 
    this.ChartObj.ChartAreas["ChartArea1"].AxisY.Maximum             = maxY;
    this.ChartObj.ChartAreas["ChartArea1"].AxisY.Interval            = interval;
    this.ChartObj.ChartAreas["ChartArea1"].AxisY.IsLogarithmic       = false;
    this.ChartObj.ChartAreas["ChartArea1"].AxisY.Title               = "Reputation Points";
    this.ChartObj.ChartAreas["ChartArea1"].AxisY.MajorGrid.LineColor = Color.Silver;
    this.ChartObj.ChartAreas["ChartArea1"].AxisX.MajorGrid.LineColor = Color.Silver;
    this.ChartObj.ChartAreas["ChartArea1"].AxisX.LabelStyle.Format   = "MMMdd";
    this.ChartObj.ChartAreas["ChartArea1"].AxisX.IntervalType        = DateTimeIntervalType.Days;

    TickMark tickMajorY      = new TickMark();
    tickMajorY.Interval      = interval * 0.2;
    tickMajorY.TickMarkStyle = TickMarkStyle.OutsideArea;
    tickMajorY.LineColor     = Color.Black;

    this.ChartObj.ChartAreas["ChartArea1"].AxisY.MajorTickMark = tickMajorY;
    this.ChartObj.AntiAliasing = AntiAliasingStyles.All;
}

private Series CreateSeries(RepCategory category, List<RepItem> list)

This method creates a series based on the reputation category and the specified list of rep items for that category.

C#
//--------------------------------------------------------------------------------
private Series CreateSeries(RepCategory category, List<RepItem> list)
{
    Series series      = new Series(category.ToString(),list.Count);
    series.ChartArea   = this.ChartObj.ChartAreas[0].Name;
    series.ChartType   = SeriesChartType.Line;
    series.XValueType  = ChartValueType.Date;
    series.YValueType  = ChartValueType.Int32;
    series.Color       = m_seriesColors[category];
    series.MarkerStyle = MarkerStyle.Circle;
    series.BorderWidth = 2;

    int startingValue = 0;
    for (int j = 0; j < list.Count; j++)
    {
        RepItem item = list[j];
        if (j > 0)
        {
            int value = item.Value - startingValue;
            m_highestValue = Math.Max(m_highestValue, item.Value - startingValue);
            series.Points.AddXY(item.TimeScraped, value);
        }
        else
        {
            startingValue = item.Value;
            series.Points.AddXY(item.TimeScraped, 0);
        }
    }
    return series;
}

The Main Form

The main form in the application is where all the action takes place.

Initialization

The Constructor

The first thing we have to do is retrieve our settings (user ID and connection string) and retrieve our data from the database. Then we add our chart controls, and finally, we add event handlers for the scraper object.

C#
//--------------------------------------------------------------------------------
public Form1()
{
    RepSettings.Default.Reload();
    m_scraper = new RepScraper(false);

    Globals.UserID           = RepSettings.Default.UserID;
    Globals.ConnectionString = RepSettings.Default.ConnectionString;

    m_scraper.Reputations.GetData();

    InitializeComponent();

    AddChartControls();

    this.m_scraper.ScrapeComplete += new ScraperEventHandler(m_scraper_ScrapeComplete);
    this.m_scraper.ScrapeFail     += new ScraperEventHandler(m_scraper_ScrapeFail);
    this.m_scraper.ScrapeProgress += new ScraperEventHandler(m_scraper_ScrapeProgress);
}

private void Form1_Load(object sender, EventArgs e)

This is where we initialize all of the controls to their defaults. If the current values were saved as settings, this is probably where you'de retrieve them as well.

C#
//--------------------------------------------------------------------------------
private void Form1_Load(object sender, EventArgs e)
{
    this.dateTimePickerGoalDate.MinDate = DateTime.Now.AddDays(1).Date;
    this.comboTimePeriod.SelectedIndex  = 0;
    this.comboChart.SelectedIndex       = 0;
    this.textBoxUserID.Text             = Globals.UserID.ToString();
    InitSelectedCategories();
    this.m_initialized                  = true;
    this.buttonRecalcGoal.Enabled       = (!string.IsNullOrEmpty(this.textBoxGoalPoints.Text));
    CalculateTopSection();
    RenderChart();
}

Control Events

State Goal Calculation

statedgoal_poor.jpg

statedgoal_good.jpg

statedgoal_excellent.jpg

When you want to see if you might be able to attain a certain total point value by a certain date, you use the Stated Goal panel. After you've specified a point value, and the date by which you would like to attain that point value, you then click the Recalculate button, and this is the code that executes:

C#
//--------------------------------------------------------------------------------
private void buttonRecalcGoal_Click(object sender, EventArgs e)
{
    // retrieve the goal points and perform some snaity checks to make sure we're 
	// dealing with an integer value
    int goalPoints;
    string text = this.textBoxGoalPoints.Text.Replace(",", "");
    if (int.TryParse(text, out goalPoints))
    {
	   // retrieve the goal date and today's date
        DateTime goalDate     = this.dateTimePickerGoalDate.Value.Date;
        DateTime now          = DateTime.Now.Date;

		// get the latest total points value
        int      latestValue  = m_scraper.Reputations.GetLatestPointValue(RepCategory.Total);

		// calculate the average total points per day
        int      avgValue     = m_scraper.Reputations.GetDailyAverage(RepCategory.Total);

		// do some date math to find out how many days there are between today 
		// and the goal date
        TimeSpan span         = goalDate - now;

		// calculate the number of points you might have by the stated goal date
        int      futurePoints = latestValue + (avgValue * span.Days);

		// determine the difference
        int      pointsDiff   = futurePoints - goalPoints;

		// if the points difference is a suitably high number OVER the goal points, you 
		// have an excellent change of achieving the stated goal.
        if (pointsDiff > 1000)
        {
            labelGoalStatus.Text = "Excellent";
        }
		// of course, there's a window where you have a good chance of getting there
        else if (pointsDiff > -1000 && pointsDiff < 1000)
        {
            labelGoalStatus.Text = "Good";
        }
		// and then there's this
        else 
        {
            labelGoalStatus.Text = "Poor";
        }
    }
}

I'm completely aware that my determination of your chances seems completely arbitrary, because it is. If anyone has a more statistical way to calculate this, I'm all ears, so don't be shy about speaking up.

Selecting/De-selecting Reputation Categories

select_categories.jpg

The user can select which categories to show data for by checking/unchecking the categories shown in the chart configuration panel on the left side of the form. The application requires at least one category to be checked, and if the user unchecks all of the categories, a message is displayed, and the Total category is automatically checked. The chart is then rendered.

C#
//--------------------------------------------------------------------------------
private void checkedLBCategories_SelectedIndexChanged(object sender, EventArgs e)
{
    if (checkedLBCategories.CheckedItems.Count == 0)
    {
        MessageBox.Show("At least one category must be checked.");
        int count = checkedLBCategories.Items.Count;
        checkedLBCategories.SetItemChecked(count - 1, true);
    }
    RenderChart();
}

Selecting the Chart

select_chart.jpg

Selecting the chart merely causes the chart area to be re-rendered. Nothing special or conditional is performed, with the exception of checking to make sure the form has been properly initialized.

C#
//--------------------------------------------------------------------------------
private void comboChart_SelectedIndexChanged(object sender, EventArgs e)
{
    if (this.m_initialized)
    {
        RenderChart();
    }
}

Selecting the Time Period

Image 13

Like selecting the chart, selecting a time category causes the chart area to be re-rendered if the form has been initialized. Additionally, the two DateTimePicker controls below the combo box are enabled if "Specified Period" is selected.

C#
//--------------------------------------------------------------------------------
private void datePickerTo_ValueChanged(object sender, EventArgs e)
{
    if (this.m_initialized)
    {
        RenderChart();
    }
}

//--------------------------------------------------------------------------------
private void datePickerFrom_ValueChanged(object sender, EventArgs e)
{
    this.datePickerTo.MinDate = this.datePickerFrom.Value;
    if (this.m_initialized)
    {
        RenderChart();
    }
}

Showing Trend Lines

trend_lines.jpg

When you check the Show trend lines... CheckBox, the chart re-displays the currently selected data as trend lines. Once again, all of the minutia related to setting turning trend lines on/off are abstracted out to other methods and classes (already discussed).

C#
//--------------------------------------------------------------------------------
private void checkBoxTrendLines_CheckedChanged(object sender, EventArgs e)
{
    if (this.m_initialized)
    {
        RenderChart();
    }
}

Chart Rendering

There is very little done in the form itself regarding chart rendering. We simply gather the data we need from the controls in the chart configuration panel, and populate/render the selected chart. All the nitty-gritty stuff has already been discussed elsewhere in this article series.

C#
//--------------------------------------------------------------------------------
private void RenderChart()
{
    DisplayCategories categories = GatherDisplayCategories();

    string   chartName  = comboChart.SelectedItem.ToString();
    RepChart chart      = m_charts.GetChartByName(chartName);
    string   timePeriod = this.comboTimePeriod.SelectedItem.ToString();
    DateTime from       = (timePeriod == "Specified Period") ? this.datePickerFrom.Value.Date : new DateTime(0);
    DateTime to         = (timePeriod == "Specified Period") ? this.datePickerTo.Value.Date : DateTime.Now;

    chart.ShowTrendLine = this.checkBoxTrendLines.Checked;
    chart.PopulateChart(m_scraper, timePeriod, from, to, categories);
    m_charts.ShowChart(chartName);
}

Occasionally, we need to recalculate the data displayed in the ListViews at the top of the form. Most of the code in this method involves creating the ListViewItems.

C#
//--------------------------------------------------------------------------------
private void CalculateTopSection()
{
    this.listviewCurrentPoints.Items.Clear();
    foreach (RepCategory category in Enum.GetValues(typeof(RepCategory)))
    {
        if (category != RepCategory.Unknown)
        {
            int latestValue = m_scraper.Reputations.GetLatestPointValue(category);
            int avgValue    = m_scraper.Reputations.GetDailyAverage(category);
            ListViewItem lvi = new ListViewItem(category.ToString());
            lvi.SubItems.Add(new ListViewItem.ListViewSubItem(lvi, string.Format("{0:#,#}", latestValue)));
            lvi.SubItems.Add(new ListViewItem.ListViewSubItem(lvi, string.Format("{0:#,#}", avgValue)));
            this.listviewCurrentPoints.Items.Add(lvi);
        }
    }

    this.listviewEarnedPoints.Items.Clear();
    foreach (RepPeriod period in Enum.GetValues(typeof(RepPeriod)))
    {
        int value;
        int projected;
        m_scraper.Reputations.GetCurrentPeriodPoints(period, RepCategory.Total, out value, out projected);
        ListViewItem lvi = new ListViewItem(period.ToString());
        lvi.SubItems.Add(new ListViewItem.ListViewSubItem(lvi, string.Format("{0:#,#}", value)));
        lvi.SubItems.Add(new ListViewItem.ListViewSubItem(lvi, string.Format("{0:#,#}", projected)));
        this.listviewEarnedPoints.Items.Add(lvi);
    }
}

Cleanup on Aisle 4

Don't fear the destructor. There's absolutley no reason we can't use them, so every once in a while, I invite poisonous barbs of criticism by bucking the trend. I guess that's why they call me "Outlaw Programmer".

C#
//--------------------------------------------------------------------------------
~Form1()
{
    this.m_scraper.ScrapeComplete -= new ScraperEventHandler(m_scraper_ScrapeComplete);
    this.m_scraper.ScrapeFail     -= new ScraperEventHandler(m_scraper_ScrapeFail);
    this.m_scraper.ScrapeProgress -= new ScraperEventHandler(m_scraper_ScrapeProgress);
}

The End - FINALLY!

Here we are, at the end. I'm sure you're not nearly as happy about it as I am (I had to do all the work, after all). Anyway, If you're even the slightest bit interested in keeping an eye on your reputation status, this app should help quite a bit, and maybe one day, when Codeproject gives us a Silverlight app to do this, it will have some/most/all of the same features as this application.

bacon_sandwich.jpg

Now that you've digested dinner, you may have a bacon sandwich for dessert. YUMMY!

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

Major Revision 1

CodeProject Caching

A couple of days ago, I noticed that the total category displayed on the daily change (column) chart seemed a bit - well - out of whack. It was almost twice what it should have been. Upon investigation, I found that it was in fact pulling the data on the page and saving it, but when calculating the changed amount (which it was also doing correctly), it was simply the wrong value. I had been bitten by the Codeproject caching issue where the numbers don't necessarily add up (even manually doing the math on the numbers scraped from the page was showing a variation in the mathematical total vs. the one showing on the web page. What to do?

My solution was to change the application to accept an optional command line parameter that causes the program to adjust existing Total RepItem objects, and if adjusted update the database. The changes were as follows:

  • Reputationator.Program.cs - add an args parameter to the Main method, as well as a switch stament to handle the possible arguments.

  • ReputationLib.Globals.cs - add a variable that's set by main when the appropriate parameter has been specified.

  • ReputationLib.RepCollections.cs - add code to looks for the Globals variable when loading data from the database, and if true, executes a new method that will adjust the existing total values to be mathematically correct vs what the web page reports, and then update the database with the new values. Additionally, the method that updates the database was modified to head this problem off in future updates.

Retrieving Data Manually

The form has a button that allows you to scrape your data at any time. When performed, the chart should have been updated to reflect the new data. A Y-Axis position was in fact being added, but the data wasn't showing up. The workaround was to shut the app down, and start it back up again, allowing it to load the data from the database and thus properly represent the data in the chart. With this change, the workaround should no longer be necessary.

User-Reported Errors

The following problems were reported by Simon Bang Terkildsen, and have been addressed:

  • When you enter another user id, then it's still the user id stored in the config file that is used.

  • Scraping the web page was confusing Author with Authority because of the similar spelling.

  • Countries where comma is not the thousands separator was causing parsing issues in the stated goal points field.

New Feature!

I added a new panel to the top section on the right side labeled You Vs The Leader. What it shows is the approximate date at which you will overtake the current points leader. The longer you use Reputationator, the more accurate this number will become. For the first few days, you may notice fairly erratic differences, but as the average points earned by you and the leader evens out (which will happen over longer periods) the more stable this date will become.

WPF!

I added a mostly-completed WPF version of the Reputationator app, along with an associated Part 4 of this article series.

History

  • 20 Aug 2011 : Revision 1 (see section above)
  • 14 Aug 2011 - Original article

License

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


Written By
Software Developer (Senior) Paddedwall Software
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

 
Question+5 Pin
Mike Hankey21-Aug-11 19:17
mveMike Hankey21-Aug-11 19:17 
GeneralMy vote of 5 Pin
linuxjr20-Aug-11 3:01
professionallinuxjr20-Aug-11 3:01 
QuestionWPF Version [modified] Pin
#realJSOP17-Aug-11 8:13
mve#realJSOP17-Aug-11 8:13 
General+5 + two small bugs [modified] Pin
Simon Bang Terkildsen16-Aug-11 14:32
Simon Bang Terkildsen16-Aug-11 14:32 
GeneralRe: +5 + two small bugs Pin
#realJSOP17-Aug-11 2:02
mve#realJSOP17-Aug-11 2:02 
GeneralRe: +5 + two small bugs Pin
#realJSOP20-Aug-11 2:36
mve#realJSOP20-Aug-11 2:36 
GeneralRe: +5 + two small bugs Pin
Simon Bang Terkildsen20-Aug-11 3:59
Simon Bang Terkildsen20-Aug-11 3:59 
GeneralMy vote of 5 Pin
Marc A. Brown16-Aug-11 3:48
Marc A. Brown16-Aug-11 3:48 
GeneralRe: My vote of 5 Pin
#realJSOP16-Aug-11 4:16
mve#realJSOP16-Aug-11 4:16 
GeneralMy vote of 5 Pin
Toniyo Jackson16-Aug-11 2:23
Toniyo Jackson16-Aug-11 2:23 
GeneralMy vote of 5 Pin
Nagy Vilmos16-Aug-11 2:14
professionalNagy Vilmos16-Aug-11 2:14 
QuestionNew Feature Coming Pin
#realJSOP16-Aug-11 2:02
mve#realJSOP16-Aug-11 2:02 
QuestionWPF Pin
kiquenet.com16-Aug-11 0:23
professionalkiquenet.com16-Aug-11 0:23 
AnswerRe: WPF Pin
#realJSOP16-Aug-11 1:52
mve#realJSOP16-Aug-11 1:52 
AnswerRe: WPF Pin
#realJSOP16-Aug-11 7:07
mve#realJSOP16-Aug-11 7:07 
AnswerRe: WPF Pin
#realJSOP16-Aug-11 9:38
mve#realJSOP16-Aug-11 9:38 
Questionnice Pin
Sacha Barber14-Aug-11 22:58
Sacha Barber14-Aug-11 22:58 
AnswerRe: nice Pin
Nagy Vilmos16-Aug-11 2:28
professionalNagy Vilmos16-Aug-11 2:28 
GeneralRe: nice Pin
#realJSOP16-Aug-11 2:34
mve#realJSOP16-Aug-11 2:34 
GeneralRe: nice Pin
Sacha Barber16-Aug-11 5:09
Sacha Barber16-Aug-11 5:09 
GeneralRe: nice Pin
Sacha Barber16-Aug-11 5:10
Sacha Barber16-Aug-11 5:10 
GeneralRe: nice Pin
#realJSOP16-Aug-11 7:09
mve#realJSOP16-Aug-11 7:09 
GeneralRe: nice Pin
Sacha Barber16-Aug-11 7:16
Sacha Barber16-Aug-11 7:16 
QuestionMy Vote of 1 Pin
Garth J Lancaster14-Aug-11 12:30
professionalGarth J Lancaster14-Aug-11 12:30 

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

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