Download Reputationator.zip - 1.19 MB
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
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
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
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
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
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
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).
public static Dictionary<RepCategory, Color> m_seriesColors = new Dictionary<RepCategory,Color>();
Then we setup a common font for chart titles.
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?).
public Chart ChartObj { get; private set; }
Then we have a custom event that's used to signify that there's no data to display.
public event NoDataEventHandler NoDataEvent = delegate{};
And finally, some incidental stuff that makes our day a little easier.
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.
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.
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:
protected virtual List<RepItem> GetSeriesList(RepItemCollection repItems,
string comboTimePeriod,
RepCategory category,
DateTime dateFrom,
DateTime dateTo)
{
if (repItems.Count <= 0)
{
throw new Exception(string.Format("No reputation items found. ({0} / {1})",
comboTimePeriod, category.ToString()));
}
if (category == RepCategory.Unknown)
{
throw new Exception(string.Format("Invalid reputation cateory. ({0} / {1})",
comboTimePeriod, category.ToString()));
}
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()));
}
if (dateTo.Ticks == 0)
{
throw new Exception(string.Format("Invalid end date (dateTo) used for retrieving series data. ({0} / {1})",
comboTimePeriod, category.ToString()));
}
if (comboTimePeriod != "Specified Period" && dateFrom.Ticks > 0)
{
dateFrom.AddTicks(dateFrom.Ticks * -1);
}
Next, we adjust the starting date based on the specified time period.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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
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:
private void buttonRecalcGoal_Click(object sender, EventArgs e)
{
int goalPoints;
string text = this.textBoxGoalPoints.Text.Replace(",", "");
if (int.TryParse(text, out goalPoints))
{
DateTime goalDate = this.dateTimePickerGoalDate.Value.Date;
DateTime now = DateTime.Now.Date;
int latestValue = m_scraper.Reputations.GetLatestPointValue(RepCategory.Total);
int avgValue = m_scraper.Reputations.GetDailyAverage(RepCategory.Total);
TimeSpan span = goalDate - now;
int futurePoints = latestValue + (avgValue * span.Days);
int pointsDiff = futurePoints - goalPoints;
if (pointsDiff > 1000)
{
labelGoalStatus.Text = "Excellent";
}
else if (pointsDiff > -1000 && pointsDiff < 1000)
{
labelGoalStatus.Text = "Good";
}
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
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.
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
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.
private void comboChart_SelectedIndexChanged(object sender, EventArgs e)
{
if (this.m_initialized)
{
RenderChart();
}
}
Selecting the Time Period
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.
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
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).
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.
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 ListView
s at the top of the form. Most of the code in this method involves creating the ListViewItem
s.
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".
~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.
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