January 16, 2012

Visual Studio 2008 Macros

I recently tried Macros in Visual Studio 2008. You can do various customization to the IDE using the Macro editor. For an example:

  • Google the selected word.
  • Display a message box after a build is completed.
  • Record an operation and play it.
  • Etc...
As an example I will be covering first two from the above.

To create Macros, go to Tools menu select Macros and then select Macros IDE. This will open up a new program (Microsoft Visual Studio Macros) and using the editor you can add macros in to the IDE.


If you haven’t used the Macro IDE before, the first thing you’ll notice is that it resembles the regular VS.NET IDE, except the Macro IDE is used solely for the creation, editing, and compiling of macros that can be used within your Visual Studio IDE.


Open up the MyMacros project and see if it already contains an EnvironmentEvents module. 

Add the following code snippet by adding a new VB code file.

    Private Const BROWSER_PATH As String = "C:\Program Files (x86)\Mozilla Firefox\firefox.exe"

    Sub SearchGoogle()
        Dim cmd As String
        cmd = String.Format("{0} http://www.google.com/search?hl-en&q={1}", BROWSER_PATH, DTE.ActiveDocument.Selection.Text)
        Shell(cmd, AppWinStyle.NormalFocus)
    End Sub


This will spawn a new Firefox Window and send the selected text to Google.


You can access Visual Studio environment (EnvDTE) instance in a Macro to do various things such as customize pre-build event or a post-build event. EnvDTE is an assembly-wrapped COM library containing the objects and members for Visual Studio core automation.


For an example the following code displays an message box after a build is completed. For this you hook into the OnBuildDone event the same way you did for the project build complete.



    Private Sub BuildEvents_OnBuildDone(ByVal Scope As EnvDTE.vsBuildScope, ByVal Action As EnvDTE.vsBuildAction) Handles BuildEvents.OnBuildDone
        'Alert that we finished building!
        If DTE.MainWindow.WindowState = EnvDTE.vsWindowState.vsWindowStateMinimize Or DTE.MainWindow.Visible = False Then
            System.Windows.Forms.MessageBox.Show("Build is complete!", _
                IO.Path.GetFileNameWithoutExtension(DTE.Solution.FullName), _
                MessageBoxButtons.OK, _
                MessageBoxIcon.Information, MessageBoxDefaultButton.Button1, _
                MessageBoxOptions.DefaultDesktopOnly)
        End If
    End Sub


These are some pretty simple examples, but you have the power of the .NET Framework in your hands for any of these events so you are limited only by what you can come up with to handle.


The post build event will automatically run after a build if the IDE is invisible or minimized. However to run the Google macro, you can assign this Macro to a toolbar by going into Tools, Customize, Command, Macros and drag and dropping SearchGoogle function.




January 10, 2012

Sprint Burndown Charts

This article shows how to draw sprint burndown charts using Dojo. Here is the JavaScript code:

// Some globals.
defaultTitleColor = "darkblue";
defaultXAxisTitle = "Days";
defaultYAxisTitle = "Remaining / Verified Tasks";
dojoInitialized = false;
chartArgs = new Array();
holidayDatesArray = new Array();

var themeClasses = new Object();

// Some utility functions.
$.getScript = function(url, callback, cache){
 $.ajax({
   type: "GET",
   url: url,
   success: callback,
   dataType: "script",
   cache: cache
 });
};

var addBusinessDays = function(d, n) {
 d = new Date(d.getTime());
 var day = d.getDay();
 d.setDate(d.getDate() + n + (day === 6 ? 2 : + !day) + (Math.floor((n - 1 + (day % 6 || 1)) / 5) * 2));
 return d;
}

var setDayValues = function (seriesIndex, days) {
 for (var i = 0; i < days; i++) {
  this.data.setValue(i, seriesIndex, "" + (i + 1));
 }
}

var datesEqual = function(a, b) {
   return (!(a > b || b > a));
}

var setDayLabels = function(startDate, days, dayLabels) {
 var daysSkiped = 0;
 for (var i = 0; i < days + 1; i++) {
  if (i == 0) {
   dayLabels.push({value: i + 1, text: ""});
   continue;
  }
  
  // We need to skip the weekends.
  var date = addBusinessDays(new Date(startDate), i - 1 + daysSkiped);
  
  // We need to skip the holidays.
  var isHoliday = false;
  while (holidayDatesArray != null && !isHoliday) {
   for (var j = 0; j < holidayDatesArray.length; j++) {
    if (datesEqual(date, holidayDatesArray[j])) {
     isHoliday = true;
     break;
    }
   }
   
   if (isHoliday) {
    date = addBusinessDays(date, 1);
    daysSkiped++;
    isHoliday = false;
   } else {
    break;
   }
  }

  var label = date.toDateString();
  label = label.substring(label.indexOf(" ") + 1, label.lastIndexOf(" "));
  dayLabels.push({value: i + 1, text: label});
 }
}

// A function to initialize the chart.
var initChart = function(chart, tensionValue, animationDuration, startDate, days, useDayLabels, xAxisTitle, yAxisTitle, theme) {
 var dayLabels = [];
 var xAxisLabelsRotationValue = 0;
 
 // Set the x axis labels. We do this only if the client tell us to do so.
 // Otherwise the default values will be used, which is 1, 2, 3, ...
 if (useDayLabels) {
  xAxisLabelsRotationValue = -15;
  setDayLabels(startDate, days, dayLabels);
 }
 
 // Set the chart theme.
 chart.setTheme(themeClasses[theme.toLowerCase()]);

 // Set the chart plots and axies.
 chart.addPlot("default", {
  type: "Lines", 
  animate: { 
   duration: animationDuration, 
   easing: dojox.fx.easing.linear}, 
  markers: true, 
  tension: tensionValue}
 );
 
 chart.addAxis("x", {
  minorTicks: true, 
  minorTickStep: 1, 
  stroke: "gray", 
  font: "normal normal normal 8pt Tahoma", 
  title: xAxisTitle, 
  titleOrientation: "away", 
  titleFontColor: "gray", 
  labels: dayLabels, 
  maxLabelSize: 80, 
  rotation: xAxisLabelsRotationValue}
 );
 
 chart.addAxis("y", {
  vertical: true, 
  includeZero: true, 
  minorTicks: false, 
  stroke: "gray", 
  font: "normal normal normal 8pt Tahoma", 
  title: yAxisTitle, 
  titleFontColor: "gray"}
 );
}

// Sets the values for ideal burndown.
var getIdealBurndownData = function(days, tasksRemainingInitValue) {
 var values = new Array();
 for (var i = 0; i < days + 1; i++) {
  values.push(tasksRemainingInitValue - (tasksRemainingInitValue / days) * i);
 }
 return values;
}

// Sets the values for a data series.
var plotChartData = function(chart, args) {
 chart.addSeries("Ideal Breakdown", getIdealBurndownData(args.days, args.tasksRemaining[0]));
 chart.addSeries("Remaining Tasks", args.tasksRemaining);
 chart.addSeries("Verified Tasks", args.tasksVerified);
}

// Initializes the holiday dates array from a string array.
var stringsToDates = function(strings, dates) {
 for (var i = 0; i < strings.length; i++) {
  dates.push(new Date(strings[i]));
 }
}

// Draws the burndown chart.
var drawBurndownInternal = function(args) {
 // Initialize the variables.
 var titleColor = args.titleColor;
 if (!titleColor) titleColor = defaultTitleColor;
 
 var chartName = "burndownChart";
 if (args.chartName) chartName = args.chartName;
 
 var legendName = "burndownChartLegend";
 if (args.legendName) legendName = args.legendName;
 
 var xAxisTitle = args.xAxisTitle;
 if (!xAxisTitle) xAxisTitle = defaultXAxisTitle;
 
 var yAxisTitle = args.yAxisTitle;
 if (!yAxisTitle) yAxisTitle = defaultYAxisTitle;

 var tensionValue = "S";
 if (args.curvedLines == false) tensionValue = "";
 
 var animationDuration = 500;
 if (args.animate == false) animationDuration = 0;
 
 var useDayLabels = true;
 if (!isNaN(args.useDayLabels)) useDayLabels = args.useDayLabels;
 
 if (args.holidays != null && args.holidays.length > 0) {
  stringsToDates(args.holidays, holidayDatesArray);
 }
 
 var theme = "WatersEdge";
 if (args.theme) theme = args.theme;

 // Create the chart object.
 var burndownChart = new dojox.charting.Chart2D(chartName, {
  title: args.title,
  titlePos: "top",
  titleGap: 25,
  titleFont: "normal normal bold 12pt Arial",
  titleFontColor: titleColor});

 initChart(burndownChart, tensionValue, animationDuration, args.startDate, args.days, useDayLabels, xAxisTitle, yAxisTitle, theme);
 plotChartData(burndownChart, args);

 // Set tooltips, magnification, etc...
 new dojox.charting.action2d.Magnify(burndownChart, "default", {scale: 2});
 //new dojox.charting.action2d.Tooltip(burndownChart, "default");
 //new dojox.charting.action2d.Highlight(burndownChart, "default");
 
 // Render the chart.
 burndownChart.render();

 // Create the legend.
 var legend = new dojox.charting.widget.Legend({
   chart: burndownChart, 
   horizontal: false, 
   style: "font-family:arial;color:black;font-size:10px;"}, 
  legendName);
}

// The function gets called after the Dojo is initialized.
var onDojoInitialized = function() {
 dojoInitialized = true;
 
 themeClasses["wetland"] = dojox.charting.themes.Wetland;
 themeClasses["tufte"] = dojox.charting.themes.Tufte;
 themeClasses["shrooms"] = dojox.charting.themes.Shrooms;
 themeClasses["watersedge"] = dojox.charting.themes.WatersEdge;
 
 for (var i = 0; i < chartArgs.length; i++) {
  drawBurndownInternal(chartArgs[i]);
 }
}

// The clients call this function to draw the burndown charts.
var drawBurndown = function(args) {
 if (!dojoInitialized) {
  $.getScript('http://ajax.googleapis.com/ajax/libs/dojo/1.6.0/dojo/dojo.xd.js', function() {
   dojo.require("dojo.fx");
   dojo.require("dojox.fx.easing");
   dojo.require("dojox.charting.Chart2D");
   dojo.require("dojox.charting.action2d.Magnify");
   dojo.require("dojox.charting.action2d.Tooltip");
   dojo.require("dojox.charting.action2d.Highlight");
   dojo.require("dojox.charting.widget.Legend");
   dojo.require("dojox.charting.themes.WatersEdge");
   dojo.require("dojox.charting.themes.Wetland");
   dojo.require("dojox.charting.themes.Tufte");
   dojo.require("dojox.charting.themes.Shrooms");
   
   // This is a HACK!
   // When I tried 2 charts on the same page, only one chart was rendered.
   // I guess this is because of the second request is not going into onDojoInitialized function.
   // So as a hack, we cache the arguments and then after the Dojo is initialized we start drawing the charts.
   chartArgs.push(args);
   
   dojo.ready(function() { onDojoInitialized(); });
  });
 } else {
  drawBurndownInternal(args);
 }
};

Here is the HTML code:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<body style="font-family: Arial;border: 0 none;">
 <script src="http://code.jquery.com/jquery-latest.js"></script>
 <script type="text/javascript" src="burndownChart.js"></script>
 <table border="0"><tr valign="top">
  <td><div id="burndownChart" style="width: 800px; height: 400px;"></div></td>
  <td><div style="width: 100px; margin-top:100px; border: 1px solid #C8C8C8; padding: 5px; background-color: rgba(255, 255, 221, 0.8);"><div id="burndownChartLegend" ></div>
  </div></td>
 </tr></table>

 <script type="text/javascript">
  drawBurndown({
   title: "Linking 2.0 Sprint 9 Burdown Chart",
   startDate: "2011/10/24",
   days: 15,
   tasksRemaining: [50,49,49,53,57,54,33,31,28,28,27,24,23,23,23,16],
   tasksVerified: [0,1,1,5,6,6,7,13,16,16,17,20,21,21,21,30]
  });
 </script> 
</body>

</html>