I was lucky enough to hear about and get to attend the inaugural Indy.Code() conference last week.  It was fantastic, with a lot of really relevant content, well prepared speakers, motivated attendees and well organized to boot.  It sounds like they’re planning on another go-around next year already.. and I highly recommend you check it out if you can!

Similar to before, I’d like to share some highlights from the sessions I attended. 🙂  I also bolded the biggest takeaway from each session, in case you want to skim even faster.

Please keep in mind this is only my paraphrasing or lessons learned from each talk, and may not be even remotely accurate of what the speaker was trying to convey.  We all hear what we want to hear.. 😉

Intro to Azure Search – Chad Campbell

  • Searches on normalized data can be painfully expensive with recursion/joins, you need to denormalize
  • Azure Search provides natural language processing, relevant scoring, recommendations, geospatial capabilities, faceted navigation and hit highlighting.
  • NLP handles verb tenses, plurals, synonyms, etc.

Azure: Move to the Cloud, not just a Remote Server – Aaron Hoffman

  • Message queues can be used as a processing queue to hold onto messages for later processing
  • Message queues utilize a two-step GetMessage/DeleteMessage pattern with automatic configurable timeout to guarantee processing completes
  • WebJobs can be setup to sleep/wake whenever a queue message arrives
  • Kudu has site extensions (e.g. log browser)

Alexa Skills Kit with Web API on Azure – Heather Downing

  • Sessions timeout in ~4.5 seconds, longer running operations are not available
  • Custom skill responses include both the voice response as well as the card content to be shown in the Alexa app
  • Custom slots are used for placeholder values (e.g. enumerations, search phrases)
  • Super cool LCARS (GitHub source) demo was given.  Impressive!
  • Amazon actively gates publication of skills.  Recommend overriding default intents (e.g. Help, Stop, Cancel), verifying requests are authentic from Amazon and generated within the last 2.5 minutes.

What Makes You DO Stuff? The Psychology of Motivation – Arthur Doler

  • Evolutions to motivation theory have generally been driven by psychology studies
  • “It sucks to be a control group” – Arthur Doler
  • Type I: Intrinsically motivated, self-renewable resource, money is a hygiene factor, perform best long run
  • Type X: Extrinsically motivated, exhaustible resource, money is the end goal, short term performer
  • A person can be Type I in some areas and Type X in others.  Neither is right or wrong.
  • To filter Type X, offer lump sum settlement to leave two weeks after hire
  • “Hire good people and leave them alone” -William McKnight

From Developer to Data Scientist – Gaines Kergosien

  • A data scientist is better at statistics than most developers
  • A data scientist is better at development than most statisticians
  • Spends majority of their time in ETL
  • To get started, brush up on basic mathematics/statistics

What every developer needs to know about crypto – Sasha Kotlyar

  • Do not roll your own
  • Graphics cards can calculate 11 billion hashes per second
  • Use a slower hashing algorithm to slow down brute force attacks.  A user won’t mind 300ms vs. 1ms for login but huge impact to attacks.

Prioritize This! – Randy Skopecek

  • Vision is the ability to ability to see the soul/potential of the product
  • People who help progress towards the vision are the ecosystem
  • Your priorities should only come from the ecosystem, not always the same as employees/customers
  • Maturity barrier – just because an idea is good doesn’t mean it’s ready to be accomplished
  • There is no lack of communication.. UserVoice, votes, surveys, comments, etc.  We’re inundated with opinions.
  • Likes/ratings are proxies for a priority list.  Ask for the priority list directly and weight based on alignment with the vision.
  • Prioritization needs to be very timely (reset regularly)

Building Apps with Intelligence – Brian Sherwin

  • Microsoft Cognitive Services has Vision, Speech, Language, Knowledge and Search APIs
  • Can use a Raspberry PI to communicate over REST to hosted Cognitive Services as well as integrate with IoT
  • Language -> Text Analytics, review sentiment of reviews
  • Vision has been used to power glasses for the blind (Video)

Getting pushy with SignalR and Reactive Extensions – Jim Wooley

  • SignalR abstracts over WebSockets, Server Sent Events and polling
  • Browser link is SignalR driven
  • Rx is good for aggregating/filtering streams (e.g. doing things like throttling/debouncing)

ASP.NET Core’s Built-in Dependency Injection Framework – Jonathan Tower

  • Singleton, scoped (request) or transient lifetimes
  • Doesn’t work for .NET Core, just ASP.NET Core
  • Can use built-ins or externals (e.g. Autoface, StructureMap.. don’t use NInject yet)

Azure Functions and AWS Lambdas – Serverless Architecture and You – Aaron Hoffman

  • Great for sporadic, irregular workloads.. but does not replace your workhorses
  • Pros: Lower operational cost, low complexity, easy to scale
  • Cons: Relatively new, poor debugging, infrastructure/vendor lock-in limitations

What C# Programmers Need to Know About Pattern Matching – Eric Potter

  • Reused switch keyword instead of the more popular match keyword to avoid backwards compatibility break with existing programs that use the word ‘match’ as a variable (yep I’ve got a few of those in CodeMaid)
  • F# pattern matching is first class, C# is more of a bolt-on that isn’t as fleshed out
  • Classic FizzBuzz can be solved using C# 7 tuples and when clauses on case statements.

Indy.Code() 2017 Summary

I was really impressed with this conference and want to say a big thank you to the organizers, speakers, sponsors, staff and everyone who participated.. well done and thanks for a great time!  I hope to see you next year. 🙂

Starred notes from CodeMash 2017

0
January 15, 2017 // irl

It’s the start of a new year which means it’s CodeMash season and I’d like to share some highlights from the sessions I attended. 🙂  I also bolded the biggest takeaway from each session, in case you want to skim even faster.

Please keep in mind this is only my paraphrasing or lessons learned from each talk, and may not be even remotely accurate of what the speaker was trying to convey.  We all hear what we want to hear.. 😉

Introduction to Angular 2 (Part 1) – Jonathan DeJong

  • Binding directionality is explicit vs. Angular 1.x
    [] – JavaScript to HTML
    () – HTML to JavaScript
    [()] – Two Way
  • There is a ton of boilerplate and fanfare to create simple things. (my opinion)  We touched 10 files to create a list.
  • I’m sticking with Angular 1.x for now, and would strongly consider jumping to a whole new framework before going to Angular 2.x.  The learning curve would be the same.  Harsh but true! (my opinion)
  • Left early

Bring your Big Data to life using JavaScript and a browser using D3.js – Dan Shultz

  • Late arrival
  • Svg viewBox lets you control internal scaling (vs. external scaling)
  • Cross Filter is a map reduce in JavaScript
  • You certainly want to crunch anything you can server-side.. but allowing some client-side crunching allows for more dynamic (unexpected) use cases by analysts.

Unleash the computer on your data – Amber McKenzie

  • Kaggle has an amazing in-browser educational experience using Jupyter notebooks.
  • Precision: how often was your label (answer) correct? (e.g. predicted red and it really was red)
  • Recall: how often did you identify the label (answer)? (e.g. it really was red and predicted it)
  • Many ways to represent nature language text: Bag of words (yes/no word appeared), frequency count (how many times it appeared), TF-IDF (Term-Frequency, Inverse Document Frequency) frequent in your targets and infrequent in others, Word2vec (plot phrases as vectors)

Building Domain-Driven AsP.NET Core Applications – Steve Smith

  • Domain-Driven design puts your business logic as your core assemblies without dependencies
  • Inner layers define interfaces, outer layers implement them
  • Spend your dev time on your core domain (veterinary care) not on your sub-domains (billing, scheduling, etc.)
  • You may have shared components (NuGet package) but most things are in bounded contexts which have separate databases, teams, etc.

How to Win Friends and Influence People With Data Visualization – Amanda Cinnamon

  • Know your audience and target a visualization for them, don’t build complexity for all possible audiences
  • Enclosure (bounded box) is the strongest way to associate data
  • Data-ink ratio: How much data is being portrayed by how much ink.  Less is generally better.
  • Pie charts are evil (paraphrased)

Chipping away at the monolith with Go. – Aaron Salvo

  • At AOL they started builds via an AIM message to a bot
  • “go get” is not semantically versioned yet and can be a challenge
  • Design is more important than language.  A red octagon is still understood even when stop is written in another language.

That Time I Built a Thinking Robot – Seth Juarez

  • Robot scariness is an inverse of their eye size (see Johnny Five, who is indeed alive)
  • Taught a robot how to drive itself via light sensors (follow dark line)
  • Hooked up an XBox controller to easily associate labeled (answer) data for training
  • Machine learning can be done on IoT.. *mind blown*

Simplify Your Life with CQRS – Joel Mason

  • Segregate your commands (writes) and your queries (reads).. they’re fundamentally different.
  • CQRS is not so much a pattern for writing code as it is a pattern for thinking through interactions.
  • Track events instead of objects.  Allows you to trace what happened and replay as needed.
  • A system’s current state is an aggregation of its past events.

An Introduction to building websites with Aurelia and Asp.Net Core – Eric Potter

  • JavaScript Services is your friend
  • Aurelia is not opinionated, very flexible and has an opt-in model
  • ASP.NET Core has been bench marked faster than Node.JS in some conditions

Build a JavaScript Dev Environment in 1 hour – Cory House

  • Too many decisions, use a starter kit
  • Webpack is the new bundler de jour
  • Recommends: VSCode, EditorConfig, npm (yarn), Express, localtunnel, npm scripts, babel, ES6 modules, Webpack, ESLint, Anything testing, Travis or AppVeyor

Real-time Server Telemetry: The downfall of logging and rise of data pipelines – Stephen Shary

  • Have a correlation ID for all requests so you can trace things through all dependencies, even with external vendors
  • Common to cycle between feast (too many logs) and famine (not enough logs)
  • Keep all recent, aggregate older logs, eventually drop
  • Centralize your logs somewhere

The Mindful Developer: The Neuroscience of Successful Software Developers – Matthew Renze

  • Our bodies evolved to be largely relaxed, occasionally stressed
  • Modern society is inverted, constant stressors (texts, emails, notifications, alerts) with occasional relaxation
  • Mindlessness is our default behavior of reliving the past or worrying about the future, generating even more stress.
  • Mindfulness is being aware of the present with acceptance

MongoDB Aggregation like a Champ! – Nuri Halperin

  • Aggregation works as a pipeline.. filter on this, then group on that, then sort by other.
  • Group, sort, match, limit, skip, project, redact (cut depth), unwind (flatten), geonear, out, sample are available aggregate functions
  • “Sort sorts.. I don’t know what else to say about that”
  • Paging is available, but is not based on a snapshot so may get replays or miss data.
  • If you’re going to have sub documents (e.g. OrderItem), the lifetime of those should line up with the main document (e.g. Order)
  • Explain will show you the execution plan

Become a Remote Working Pro – Michael Eaton

  • Do amazing work on remote days to prove it works
  • Self promotion is harder when remote
  • Suggest everyone having a WFH day (e.g. once a week) to be cognizant of remote challenges

More Money with Less Effort: Validate App Ideas Before Coding – Scott Showalter

  • Big mistake is not sharing.. 99% of people aren’t going to steal your idea
  • Ideas are a dime a dozen, execution is what matters
  • Make ‘yes’ cost something to reviewers.. their time, reputation or money.  People just want to be nice to your feelings and are overly positive when it doesn’t cost them anything.
  • Time boxing is critical to stay low fidelity and fast

A Crash Course on Building Microservice-based Architectures – Shawn Wallace

  • Microservices are independently testable and deployable
  • Embrace async interactions and event driven architecture.  REST is not your friend, waits for a response.
  • Code re-use is overrated.
  • Data is not just at rest, also in motion.
  • RabbitMQ for event passing

.NET Standard for Mere Mortals – Jeffrey Fritz

  • .NET Standard is one API to rule them all
  • Lower versions (1.0) have fewer APIs, larger target footprint
  • Higher versions (1.6) have more APIs, smaller target footprint
  • 2.0 will have a much larger API surface and allow for referencing existing .NET Framework DLLs

Adding ES6 to Your Developer Toolbox – Jeff Strauss

  • Use Babel transpile to get the good stuff today without waiting for browers
  • Template literals (string interpolation) `This is ${expression}`  (expression = ‘Sparta’ of course)
  • Destructuring allows for direct assignment of multiple return values
  • Let provides the logical scope that var was always missing

Summary

A lot of effort goes into putting on CodeMash each year and I want to say thank you to the organizers, speakers, Kalahari staff and everyone who makes such an incredible event possible!

Lets say you’re using DocumentDB and find yourself migrating from single-partition to partitioned collections.

There’s an off chance you may forget to add a PartitionKey property value to all records before migration.  DocumentDB will allow you to create documents without a PartitionKey, but querying for them is a little more tricky.

With the help of Frans Lytzen’s article you’ll be able to Find docs with no PartitionKey in Azure DocumentDb.

To take things one step further, you may want to purge those records prior to re-running your import. If that’s the case, here’s an example of the code you may need:


        private static async Task<int> PurgeUnpartitionedDocuments(IDocumentClient client, Uri collectionUri)
        {
            var documentQuery = client.CreateDocumentQuery<Document>(collectionUri, new FeedOptions { PartitionKey = new PartitionKey(Undefined.Value) }).AsDocumentQuery();
            int count = 0;

            while (documentQuery.HasMoreResults)
            {
                foreach (Document document in await documentQuery.ExecuteNextAsync())
                {
                    await client.DeleteDocumentAsync(document.SelfLink, new RequestOptions { PartitionKey = new PartitionKey(Undefined.Value) });
                    count++;
                }
            }

            return count;
        }

Hope it helps! Thanks again to Frans for his very helpful article. 🙂

Starred notes from CodeMash 2016

0
January 10, 2016 // irl

I had another great experience at CodeMash this year and would like to once again share some highlights.

Please keep in mind this is only my paraphrasing or lessons learned from each talk, and may not be even remotely accurate of what the speaker was trying to convey.  We all hear what we want to hear.. 😉

7 Languages in 7 Hours – Amber Conville

  • We covered Ruby, Clojure, Haskell, Rust, Scala, Elixir and Go – exercising the same kata in each
  • Ruby was the most elegant and simple (my opinion)
  • Clojure was the most awkward with its ((heavy (use) of (parentheses)) (my (opinion(s))))
  • When you exercise the same simple kata 7 times, the differences between the languages become a lot more muted and the algorithms/approaches stand out

[Factory] Fun with Machine Learning – Jennifer Marsman

  • 3 primary classes of algorithms
    • Classification (Discrete answers)
    • Regression (Continuous answers)
    • Clustering (Answer unknown, group things that are alike together)
  • Azure has made it crazy easy to get up and running quickly
  • Domain knowledge is really helpful to sniff test results

Humanitarian Toolbox Hack for Good (Day 2, Part 2) – Tony Surma

  • ASP.NET 5 is back to the command line (currently, rumor has it that it’s changing)
  • My past AppVeyor/NPM experience is reusable
  • It’s easy to step in and solve detailed technical problems, without broader context

Node.js Crash Course – David Neal

  • Node.JS utilizes an asynchronous single-threaded message pump, forcing you into pit of success (callback/async patterns)
  • node-inspector lets you debug through Chrome
  • Edge.JS will let you run .NET code in a Node.JS process

Building Angular 2.0 Applications with TypeScript – Bill Wagner

  • Angular 2.0 is built using TypeScript
  • Can set breakpoints on TypeScript source within the browser
  • There’s a lot of special characters in use (parentheses, brackets, asterisks, pounds)
  • Main benefits of TypeScript are avoiding injector errors (typos) and better IntelliSense

Designing C# 7 in the Open – Kevin Pilch-Bisson

  • Limit design meetings to 8-10 people to avoid design by (large) committee
  • ~40 people work on Roslyn
  • Considering a lot of features.. a few particularly interesting ones:
    • Non-null by default
    • Records (i.e. very basic class type)
    • Strong types for data over the wire (e.g. JSON)
    • Pattern matching / switching on type

From Developer to Manager – Sean O’Connor

  • Coding is not doing your job
  • Management is a learn-able skill, just like a new language
  • Management is about setting the tone for the team
  • While tempting to let issues self-resolve, need to be very wary about letting things fester

Design for Non-Designers from a Non-Designer – Jay Harris

  • Primary principle of design is usability, which is familiarity
  • There are rules and they can be broken, but as a novice follow them
  • Developers hate repetition (monochrome), and want to abstract it into a class 😉
  • Contrast will always draw the eye, control the contrast in your design

Why I Left Angular and Knockout for React – Cory House

  • React is not a framework, it’s a library
  • React puts JavaScript and HTML into the same file, as they’re closely coupled
  • React only supports one-way flow to force developer to think through data flow
  • Angular embraces HTML, React embraces JS

You CAN Kanban – Steve Smith

  • 2 rules – Visualize the work, minimize in progress
  • Optimize for flow (time from conception to completion) over utilization (maxing out the developers, which leads to context switching and inefficiencies)
  • When prioritizing, avoid low/medium/high.. you’ll never do anything but high

ASP.NET 5: How to Get Your Cheese Back – Tugberk Ugurlu

  • Complete rewrite, only “concept compatibility”
  • Project content is inherent from folders (no .csproj)
  • Project output is explicit in webroot folder
  • Configuration hierarchy is explicitly defined, no more transforms
  • No ties to msbuild at all, gulp instead for client-side building

Art And Code: Make Useless Things – Jamison Dance

  • Art recognizes value in the process of creation, and in the thing that is created in itself – not just in the utility of what the creation does which is how we generally evaluate code
  • Code can be art, with or without great utility
  • All of the worst things in coding (estimation, status meetings, micro management) can be jettisoned so the best things in coding (creation from nothing, imagination) can flourish when making something useless
  • Happiness and laughter are worth creating, so useless things may not be actually useless at all
  • If you only code for utility, you are missing out on opportunities

How to Add S.P.I.C.E. to Your Speaking – Maureen Zappala

  • Something memorable gets revisited
  • We all have stories, we’re just not convinced of their value
  • Most important process of a speech is the thought process the audience undertakes
  • Vocal variety and body language are important

Software Development Lessons Learned from Industrial Failures in the 1980s – Charlotte Chang

  • US GM automotive plant in Fremont, CA in 60’s-70’s – one of the worst work forces in automotive
  • Closed plant and opened a new one following Toyota processes/principles
  • Same individuals that were horrible and dissatisfied in the former, were successful and happy in the latter.
  • Process can transform people.

Program some health into your life – Stan Jonsson

  • You can train yourself to eat almost anything if you give it a month
  • Slip-ups happen – not a reason to slip up more
  • The best cardio is the one you find most enjoyable
  • Weight training steers your body to burn fat over muscle
  • Open debate in weight lifting about 1 set providing 80% of the value of 4 sets
  • Need to keep changing routines, body adjusts and you don’t get the same value

Cross-Platform Desktop Apps with Electron – David Neal

  • Electron apps are written in HTML/CSS/JS
  • Electron apps are packaged/run within Node.JS + Chrome
  • You don’t need permission to be awesome

Summary

If you haven’t been to CodeMash, you should check it out.  There’s a great community, great content and a very modest cost.  My sincere appreciation and gratitude to all of the speakers, organizers and everyone who helps participate to make it all possible. 🙂

WPF TreeView with Multi Select

3
November 22, 2015 // wpf

The WPF TreeView is very powerful, but out of the box it doesn’t support selecting multiple items at once.  By multiple selection I mean holding down the control key or shift key to select a set of items, similar to behavior available in other controls such as Windows Explorer.

To support enhancements like dragging and dropping multiple items at once to CodeMaid‘s Spade tool window, we wanted this capability to multi select.  There’s several different solutions out there, and in particular our implementation is largely based on the great work of Cristoph Gattnar available here.

Christoph’s solution utilizes attached properties.  Our solution converted this to a behavior and added support for arrow keys and the space bar for more keyboard navigation scenarios.

Here’s a little example of it working in action:

Multi-Select

Multi-Select

The living code can be found within CodeMaid’s open source repository here.

And just in case, here’s a static snapshot of the code:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace SteveCadwallader.CodeMaid.UI
{
    ///

<summary>
    /// A behavior that extends a <see cref="TreeView"/> with multiple selection capabilities.
    /// </summary>


    /// <remarks>
    /// Largely based on http://chrigas.blogspot.com/2014/08/wpf-treeview-with-multiple-selection.html
    /// </remarks>
    public class TreeViewMultipleSelectionBehavior : Behavior<TreeView>
    {
        #region SelectedItems (Public Dependency Property)

        ///

<summary>
        /// The dependency property definition for the SelectedItems property.
        /// </summary>


        public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register(
            "SelectedItems", typeof(IList), typeof(TreeViewMultipleSelectionBehavior));

        ///

<summary>
        /// Gets or sets the selected items.
        /// </summary>


        public IList SelectedItems
        {
            get { return (IList)GetValue(SelectedItemsProperty); }
            set { SetValue(SelectedItemsProperty, value); }
        }

        #endregion SelectedItems (Public Dependency Property)

        #region AnchorItem (Private Dependency Property)

        ///

<summary>
        /// The dependency property definition for the AnchorItem property.
        /// </summary>


        private static readonly DependencyProperty AnchorItemProperty = DependencyProperty.Register(
            "AnchorItem", typeof(TreeViewItem), typeof(TreeViewMultipleSelectionBehavior));

        ///

<summary>
        /// Gets or sets the anchor item.
        /// </summary>


        private TreeViewItem AnchorItem
        {
            get { return (TreeViewItem)GetValue(AnchorItemProperty); }
            set { SetValue(AnchorItemProperty, value); }
        }

        #endregion AnchorItem (Private Dependency Property)

        #region IsItemSelected (TreeViewItem Attached Property)

        ///

<summary>
        /// The dependency property definition for the IsItemSelected attached property.
        /// </summary>


        public static readonly DependencyProperty IsItemSelectedProperty = DependencyProperty.RegisterAttached(
            "IsItemSelected", typeof(bool), typeof(TreeViewMultipleSelectionBehavior),
            new FrameworkPropertyMetadata(OnIsItemSelectedChanged));

        ///

<summary>
        /// Gets the IsItemSelected value from the specified target.
        /// </summary>


        /// <param name="target">The target.</param>
        /// <returns>The value.</returns>
        public static bool GetIsItemSelected(TreeViewItem target)
        {
            return (bool)target.GetValue(IsItemSelectedProperty);
        }

        ///

<summary>
        /// Sets the IsItemSelected value on the specified target.
        /// </summary>


        /// <param name="target">The target.</param>
        /// <param name="value">The value.</param>
        public static void SetIsItemSelected(TreeViewItem target, bool value)
        {
            target.SetValue(IsItemSelectedProperty, value);
        }

        ///

<summary>
        /// Called when the IsItemSelected dependency property has changed.
        /// </summary>


        /// <param name="obj">The dependency object where the value has changed.</param>
        /// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void OnIsItemSelectedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var treeViewItem = obj as TreeViewItem;
            var treeView = treeViewItem?.FindVisualAncestor<TreeView>();
            if (treeView != null)
            {
                var behavior = Interaction.GetBehaviors(treeView).OfType<TreeViewMultipleSelectionBehavior>().FirstOrDefault();
                var selectedItems = behavior?.SelectedItems;
                if (selectedItems != null)
                {
                    if (GetIsItemSelected(treeViewItem))
                    {
                        selectedItems.Add(treeViewItem.DataContext);
                    }
                    else
                    {
                        selectedItems.Remove(treeViewItem.DataContext);
                    }
                }
            }
        }

        #endregion IsItemSelected (TreeViewItem Attached Property)

        #region Behavior

        ///

<summary>
        /// Called after the behavior is attached to an AssociatedObject.
        /// </summary>


        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.AddHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnTreeViewItemKeyDown), true);
            AssociatedObject.AddHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnTreeViewItemMouseUp), true);
        }

        ///

<summary>
        /// Called when the behavior is being detached from its AssociatedObject, but before it has
        /// actually occurred.
        /// </summary>


        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.RemoveHandler(UIElement.KeyDownEvent, new KeyEventHandler(OnTreeViewItemKeyDown));
            AssociatedObject.RemoveHandler(UIElement.MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnTreeViewItemMouseUp));
        }

        #endregion Behavior

        #region Event Handlers

        ///

<summary>
        /// Called when a TreeViewItem receives a key down event.
        /// </summary>


        /// <param name="sender">The sender.</param>
        /// <param name="e">
        /// The <see cref="System.Windows.Input.KeyEventArgs"/> instance containing the event data.
        /// </param>
        private void OnTreeViewItemKeyDown(object sender, KeyEventArgs e)
        {
            var treeViewItem = e.OriginalSource as TreeViewItem;
            if (treeViewItem != null)
            {
                TreeViewItem targetItem = null;

                switch (e.Key)
                {
                    case Key.Down:
                        targetItem = GetRelativeItem(treeViewItem, 1);
                        break;

                    case Key.Space:
                        if (Keyboard.Modifiers == ModifierKeys.Control)
                        {
                            ToggleSingleItem(treeViewItem);
                        }
                        break;

                    case Key.Up:
                        targetItem = GetRelativeItem(treeViewItem, -1);
                        break;
                }

                if (targetItem != null)
                {
                    switch (Keyboard.Modifiers)
                    {
                        case ModifierKeys.Control:
                            Keyboard.Focus(targetItem);
                            break;

                        case ModifierKeys.Shift:
                            SelectMultipleItemsContinuously(targetItem);
                            break;

                        case ModifierKeys.None:
                            SelectSingleItem(targetItem);
                            break;
                    }
                }
            }
        }

        ///

<summary>
        /// Called when a TreeViewItem receives a mouse up event.
        /// </summary>


        /// <param name="sender">The sender.</param>
        /// <param name="e">
        /// The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance containing the
        /// event data.
        /// </param>
        private void OnTreeViewItemMouseUp(object sender, MouseButtonEventArgs e)
        {
            var treeViewItem = FindParentTreeViewItem(e.OriginalSource);
            if (treeViewItem != null)
            {
                switch (Keyboard.Modifiers)
                {
                    case ModifierKeys.Control:
                        ToggleSingleItem(treeViewItem);
                        break;

                    case ModifierKeys.Shift:
                        SelectMultipleItemsContinuously(treeViewItem);
                        break;

                    default:
                        SelectSingleItem(treeViewItem);
                        break;
                }
            }
        }

        #endregion Event Handlers

        #region Methods

        ///

<summary>
        /// Selects a range of consecutive items from the specified tree view item to the anchor (if exists).
        /// </summary>


        /// <param name="treeViewItem">The triggering tree view item.</param>
        public void SelectMultipleItemsContinuously(TreeViewItem treeViewItem)
        {
            if (AnchorItem != null)
            {
                if (ReferenceEquals(AnchorItem, treeViewItem))
                {
                    SelectSingleItem(treeViewItem);
                    return;
                }

                var isBetweenAnchors = false;
                var items = DeSelectAll();

                foreach (var item in items)
                {
                    if (ReferenceEquals(item, treeViewItem) || ReferenceEquals(item, AnchorItem))
                    {
                        // Toggle isBetweenAnchors when first item is found, and back again when last item is found.
                        isBetweenAnchors = !isBetweenAnchors;

                        SetIsItemSelected(item, true);
                    }
                    else if (isBetweenAnchors)
                    {
                        SetIsItemSelected(item, true);
                    }
                }
            }
        }

        ///

<summary>
        /// Selects the specified tree view item, removing any other selections.
        /// </summary>


        /// <param name="treeViewItem">The triggering tree view item.</param>
        public void SelectSingleItem(TreeViewItem treeViewItem)
        {
            DeSelectAll();
            SetIsItemSelected(treeViewItem, true);
            AnchorItem = treeViewItem;
        }

        ///

<summary>
        /// Toggles the selection state of the specified tree view item.
        /// </summary>


        /// <param name="treeViewItem">The triggering tree view item.</param>
        public void ToggleSingleItem(TreeViewItem treeViewItem)
        {
            SetIsItemSelected(treeViewItem, !GetIsItemSelected(treeViewItem));

            if (AnchorItem == null)
            {
                if (GetIsItemSelected(treeViewItem))
                {
                    AnchorItem = treeViewItem;
                }
            }
            else if (SelectedItems.Count == 0)
            {
                AnchorItem = null;
            }
        }

        ///

<summary>
        /// Clears all selections.
        /// </summary>


        /// <remarks>
        /// The list of all items is returned as a convenience to avoid multiple iterations.
        /// </remarks>
        /// <returns>The list of all items.</returns>
        private IEnumerable<TreeViewItem> DeSelectAll()
        {
            var items = GetItemsRecursively<TreeViewItem>(AssociatedObject);
            foreach (var item in items)
            {
                SetIsItemSelected(item, false);
            }

            return items;
        }

        ///

<summary>
        /// Attempts to find the parent TreeViewItem from the specified event source.
        /// </summary>


        /// <param name="eventSource">The event source.</param>
        /// <returns>The parent TreeViewItem, otherwise null.</returns>
        private static TreeViewItem FindParentTreeViewItem(object eventSource)
        {
            var source = eventSource as DependencyObject;

            var treeViewItem = source?.FindVisualAncestor<TreeViewItem>();

            return treeViewItem;
        }

        ///

<summary>
        /// Gets items of the specified type recursively from the specified parent item.
        /// </summary>


        /// <typeparam name="T">The type of item to retrieve.</typeparam>
        /// <param name="parentItem">The parent item.</param>
        /// <returns>The list of items within the parent item, may be empty.</returns>
        private static IList<T> GetItemsRecursively<T>(ItemsControl parentItem)
            where T : ItemsControl
        {
            if (parentItem == null)
            {
                throw new ArgumentNullException(nameof(parentItem));
            }

            var items = new List<T>();

            for (int i = 0; i < parentItem.Items.Count; i++)
            {
                var item = parentItem.ItemContainerGenerator.ContainerFromIndex(i) as T;
                if (item != null)
                {
                    items.Add(item);
                    items.AddRange(GetItemsRecursively<T>(item));
                }
            }

            return items;
        }

        ///

<summary>
        /// Gets an item with a relative position (e.g. +1, -1) to the specified item.
        /// </summary>


        /// <remarks>This deliberately works against a flattened collection (i.e. no hierarchy).</remarks>
        /// <typeparam name="T">The type of item to retrieve.</typeparam>
        /// <param name="item">The item.</param>
        /// <param name="relativePosition">The relative position offset (e.g. +1, -1).</param>
        /// <returns>The item in the relative position, otherwise null.</returns>
        private T GetRelativeItem<T>(T item, int relativePosition)
            where T : ItemsControl
        {
            if (item == null)
            {
                throw new ArgumentNullException(nameof(item));
            }

            var items = GetItemsRecursively<T>(AssociatedObject);
            int index = items.IndexOf(item);
            if (index >= 0)
            {
                var relativeIndex = index + relativePosition;
                if (relativeIndex >= 0 && relativeIndex < items.Count)
                {
                    return items[relativeIndex];
                }
            }

            return null;
        }

        #endregion Methods
    }
}

We made it back to Kalamazoo X this year for another day of soft skills, and I have to say it was even better the second time around. 🙂

Similar to the CodeMash post a couple months ago, here’s my set of bullet point take-away’s from each of the sessions.  They may be taken out of context, and are absolutely tainted with my perspective and interpretation – but hopefully they encourage you to catch these talks at other conferences.  Every one of the speakers had a great message and is worth taking the time to hear.

Note: You can also get excerpts from each of the talks at the conference site here and see a lot of chatter on Twitter at #kalx15

Jeff Blankenburg (@jeffblankenburg) – Be A Beginner

  • Beginners learn, experts know.  Wouldn’t you rather be learning?
  • Somebody being better than you, doesn’t mean you’re not an expert (i.e. there’s not only one expert).
  • After 7-8 years, inspiration fades.. then what?
  • Give permission to fail, it encourages risk taking.

Cori Drew (@coridrew) – Ignition: Geek Parenting’s Lessons Learned…so far

  • Git is a gateway drug (amen).
  • The saying “Those who can, do; those who can’t, teach” is not true at all for our industry.
  • Kids learn much better from each other than adults.
  • Let your kids epic fail, you don’t have to sugar coat everything.

Jim Holmes (@aJimHolmes) – Growing Into Leadership

  • Who can you help be more awesome?
  • You’ll seldom regret stepping away from the keyboard.
  • Give your team a safe place.
  • A leader makes their team awesome, not themselves.

Alan Stevens (@alanstevens) – Values Driven Development

  • In software, there’s always compromise.  There is no flawless system.
  • It doesn’t matter how well you build the wrong thing.
  • The standard you walk past is the standard you accept.

Cory House (@housecor) – Confessions of a Technical Speaker: A Behind-the-Scenes Tour

  • Manufacture urgency to get things done.
  • I’m not the best, nor am I the worst.  That’s OK and I can still be valuable.
  • Risk is an irreversible bad outcome.  What is truly irreversible?
  • People chose you over alternatives, they want you to succeed.

Jay Harris (@jayharris) – Conviction

  • Talent is critical, and more important than passion.
  • Find yourself at the end of each day better than when you started.
  • Success shouldn’t be hitting your goals, it should be all the milestones on the way.
  • Figure out your talents, get paid to use them.

Dawn E. Kuczwara (@digitaldawn) – Give Up!

  • To pick up something new, you must let go of something old.
  • Figure out what you are still doing that isn’t helping you to your goals.
  • Holding on to something may be blocking somebody else’s chance to grow.

Elizabeth Naramore (@ElizabethN) – The Beauty of a Beginner’s Mind

  • Todo’s are a long pointless grind.
  • The more you do something, the more your brain will grow to accommodate.
  • Time spent learning is never wasted.
  • You have 2.5hrs each day where you account to nobody.

Summary

Kalamazoo X is well worth the time, very open and inviting, and extremely affordable.  Thanks again to the speakers, participants and most of all organizers for making it so memorable. 🙂

As I’m starting to dabble with Gulp, it is wonderful to see support for it already well underway within Visual Studio.  Kudos to Microsoft/Mads Kristensen for embracing a community solution and making it compatible with the IDE we love.  There’s a lot of wonderful articles out there by Scott Hanselman, John Papa, Dave Paquette and others talking about what these tools are and how to get them up and running in Visual Studio.  I’m not going to recap any of it here as it’s already been well stated. 🙂

What I didn’t find out there was how do I take Gulp the rest of the way out to a production server?  Specifically, how can I get Gulp to run via MSbuild on a TFS2013 CI build server, and then make sure that output can be picked up and deployed via Web Deploy?  Works on my machine is great, but I <3 my CI and automated deployments.  Here’s what I learned that can take us the rest of the way to Works in my build and deployment pipeline.

Note: With the solution below, you don’t need to have the Task Runner Explorer extension by Mads Kristensen as referenced in the articles – but it can still be pretty helpful for visualizing output or ad hoc runs.

Phase 1: Conquer the universe

Whoa, whoa, slow down.  Can’t I just simply use pre-build events or post-build events?

Excellent question, let’s try it.

Pre-build events (spoiler: too soon)

Pre-build events

The commands are pretty straight-forward. We make sure we’re in the project directory on the build server, we call npm install to pull down our dependencies, and then we call out to Gulp.  This looks great, and it seems to work locally.. sometimes.

Wait, sometimes?

In my case I’m utilizing TypeScript.  What’s happening is that on a rebuild (or clean build), TypeScript has removed all the JavaScript files (or they haven’t been built yet) and there’s nothing for Gulp to find.  On a second build, Gulp finds the old JavaScript output and then it works.  Two compile passes isn’t going to fly.

Alright, pre-build events are out – let’s wait until after the build so we know all the TypeScript->JavaScript compilation is complete.

Post-build events (spoiler: too late)

Post-build events

Same setup, we’ll just try it a little later.

Now our Gulp output is getting generated (yay!).. but it happens too late to be picked up by Web Deploy which actually occurs prior to post-build (boo!).

So pre-build is too soon, and post-build is too late.  It’s time to turn to custom targets in our .csproj file.  Trust me, it sounds scarier than it really is – we’re devs, we’ve got this. 😉

Extending our .csproj with custom targets (just right)

Manually edit your .csproj file and at the bottom (must be after all Import lines) we’re going to add some new content.

  <PropertyGroup>
    <CompileDependsOn>
      $(CompileDependsOn);
      GulpBuild;
    </CompileDependsOn>
  </PropertyGroup>
  <Target Name="GulpBuild" DependsOnTargets="CompileTypeScript">
    <Exec Command="npm install" />
    <Exec Command="gulp" />
  </Target>

What we have done here is specify a PropertyGroup to extend the existing CompileDependsOn target to add our own custom target called ‘GulpBuild’.  On our target, we can specify other targets that need to happen first – such as CompileTypeScript.  Now we know that TypeScript compilation will happen first, and we’re in the pipeline long before deployment occurs.  For more details, check out this article on MSDN or Sayed Ibrahim Hashimi’s wonderful book on MSBuild.

What about triggering a gulp cleanup script when we clean our solution in Visual Studio?  It’s a very similar extension:

  <PropertyGroup>
    <CompileDependsOn>
      $(CompileDependsOn);
      GulpBuild;
    </CompileDependsOn>
    <CleanDependsOn>
      $(CleanDependsOn);
      GulpClean
    </CleanDependsOn>
  </PropertyGroup>
  <Target Name="GulpBuild" DependsOnTargets="CompileTypeScript">
    <Exec Command="npm install" />
    <Exec Command="gulp" />
  </Target>
  <Target Name="GulpClean">
    <Exec Command="gulp clean" />
  </Target>

Fantastic.  At this point, we’re now able to run Gulp both locally within Visual Studio as well as part of our build process!

Hooking into Web Deploy

So what’s left?  Well, by default Web Deploy isn’t going to pick up any of our output from the Gulp pipeline since it isn’t a part of the project.  So we need to explicitly point it at our build output.  There’s a couple helpful articles that talk about the approach in more detail on ASP.net and by Sam Stephens.

  <PropertyGroup>
    <CopyAllFilesToSingleFolderForPackageDependsOn>
      $(CopyAllFilesToSingleFolderForPackageDependsOn);
      CollectGulpOutput;
    </CopyAllFilesToSingleFolderForPackageDependsOn>
    <CopyAllFilesToSingleFolderForMsdeployDependsOn>
      $(CopyAllFilesToSingleFolderForMsdeployDependsOn);
      CollectGulpOutput;
    </CopyAllFilesToSingleFolderForMsdeployDependsOn>
  </PropertyGroup>
  <Target Name="CollectGulpOutput">
    <ItemGroup>
      <_CustomFiles Include="build\**\*" />
      <FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
        <DestinationRelativePath>build\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
      </FilesForPackagingFromProject>
    </ItemGroup>
    <Message Text="CollectGulpOutput list: %(_CustomFiles.Identity)" />
  </Target>

This is a bit harder to read, but in essence again we are extending existing targets with our own custom target. That target is collecting any files that exist in a ‘build’ folder (you can replace the 2 occurrences of ‘build’ with ‘dist’ or whatever folder convention you use), and synchronizing them across into the deployment package with the same folder structure.

Bringing it all together

Combining those pieces we’ve now got a project setup that will run Gulp both locally and on the build server, is compatible with TypeScript generating JavaScript, and is compatible with Web Deploy. If you’re raising your arms in triumph here.. thanks for joining me. 😉

  <PropertyGroup>
    <CompileDependsOn>
      $(CompileDependsOn);
      GulpBuild;
    </CompileDependsOn>
    <CleanDependsOn>
      $(CleanDependsOn);
      GulpClean
    </CleanDependsOn>
    <CopyAllFilesToSingleFolderForPackageDependsOn>
      $(CopyAllFilesToSingleFolderForPackageDependsOn);
      CollectGulpOutput;
    </CopyAllFilesToSingleFolderForPackageDependsOn>
    <CopyAllFilesToSingleFolderForMsdeployDependsOn>
      $(CopyAllFilesToSingleFolderForMsdeployDependsOn);
      CollectGulpOutput;
    </CopyAllFilesToSingleFolderForMsdeployDependsOn>
  </PropertyGroup>
  <Target Name="GulpBuild" DependsOnTargets="CompileTypeScript">
    <Exec Command="npm install" />
    <Exec Command="gulp build --mode $(ConfigurationName)" />
  </Target>
  <Target Name="GulpClean">
    <Exec Command="gulp clean" />
  </Target>
  <Target Name="CollectGulpOutput">
    <ItemGroup>
      <_CustomFiles Include="build\**\*" />
      <FilesForPackagingFromProject Include="%(_CustomFiles.Identity)">
        <DestinationRelativePath>build\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
      </FilesForPackagingFromProject>
    </ItemGroup>
    <Message Text="CollectGulpOutput list: %(_CustomFiles.Identity)" />
  </Target>

Bonus round – conditional gulp

    <Exec Command="gulp build --mode $(ConfigurationName)" />

You may have noticed I snuck something else in the final version – passing the build configuration into Gulp as a command line argument called mode. Using tools like yargs and gulpif you can then tailor your Gulp pipeline (e.g. not running uglify in ‘Debug’ mode).  If you want more details please post a comment.

Other approaches

After posting this article, I ran into two other approaches solving the same problem in slightly different ways.  All three of our posts have been in March, so clearly this is the month for automating builds. 😉  Here’s links to those alternative ways:

Summary

By utilizing custom targets in our .csproj file, we’re able to utilize npm and Gulp consistently both on our local Visual Studio instance and on our TFS build server.  We can also make sure our generated build output is picked up by Web Deploy and lands on our servers.

Thanks again to all the referenced authors and articles for guiding me to this solution.  No promises this is the only way (or even a good way) to do it, but it works for me and hopefully gives you some ideas too. 🙂  Please chime in on the comments any thoughts, questions or improvements!

Starred notes from CodeMash 2015

1
January 15, 2015 // irl

I was fortunate enough to get to attend CodeMash once again this year and wanted to share some of the great information I picked up.  These bullets are highlights from my notes and may be (i.e. definitely are) tainted with my own opinions/perspectives.  So if you dislike/disagree with anything, don’t judge the speaker blame the messenger. 😉

Metaprogramming – Kathleen Dollard

  • “Only write code that only you can write”
  • Metaprogramming is a broad subject, but specifically talked a lot about code generation via T4 and Roslyn
  • Five principles to metaprogramming – you control the results, metadata is distinct and flexible, metaprogramming should be the easiest way to get it done, human crafted code is sacred, resulting code is the highest quality
  • Good metaprogramming characteristics are very similar to code principles (e.g. DRY, SoC)
  • Consider partial classes and file extension conventions to isolate generated code from human code

Interviewing – Kerri Miller

  • Define your culture – are you a bunch of independent cowboys or do you work in a pair programming style?
  • Have people from diverse roles involved as interviewers – you’ll gain more perspectives on the candidates.
  • Write down your impressions immediately afterwards – avoid a group-think bias.
  • Consider a pair programming audition where neither of you have any foreknowledge of the problem.
  • Collect metrics – register your predictions before-hand and keep tabs on people you don’t hire (false negatives).

C# 6 – Dustin Campbell

  • Primary constructors (the syntax where you had parameters right on the class definition) have been pulled out due to complexity/confusion
  • Expression bodied members can really cut down code size when you have simple return methods
  • Extension methods may become a lot more popular as you will now be able to explicitly pull in only the set of extension methods you want with the new “using static XYZ” syntax
  • C# 6 design is complete
  • C# 7 design will begin soon, moving Roslyn from CodePlex to GitHub, finalizing VS2015

HTML5 Web Components – Cory House

  • Web components in ES6 will make it much easier to share/grab modules that bundle up HTML/CSS/JS together
  • You’ll be able to extend native HTML controls
  • There is a Shadow DOM concept that allows for areas of isolated scope that will help isolate components

Swift – Ravi Desai

  • Swift looks a lot more like Java/C/C# code now
  • Contains some quirks (e.g. two ways to do function calls) for inter-operability with Objective-C

Innovation – Dustin Updyke

  • Proposes that ideas should be tracked (e.g. Trello) and it needs to be part of your culture to foster/encourage ideas
  • Evaluate ideas on interest, value, feasibility and ability
  • “As you think so shall you become” – Bruce Lee
  • Most innovations come from rigorous examinations and hard work, not sparks in a vacuum
  • Same principles that apply within business can apply to yourself as an individual

Xamarin – Jesse Liberty

  • Xamarin platform (non-UI) gets you about 50% code reuse across mobile devices
  • Xamarin forms (UI) can get you up to about 90% code reuse
  • Xamarin forms utilizes XAML and MVVM, is extremely similar to WPF/Silverlight
  • Check out Falafel University

Roslyn – Jon Skeet

  • Roslyn can replace some unit tests you may be writing as code suggestions in VS
  • Bill Wagner is coordinating a public repository for Roslyn helpers
  • The Syntax Tree Visualizer is your friend for doing any work with Roslyn

Social Developer – Seth Juarez

  • You should punch people in the face
  • Ask meaningful questions, don’t need another weather conversation
  • If something didn’t go well, you can still ask “what did you learn”

Agile – Paul Holway

  • FDA medical device software can be done in an Agile environment
  • Cadence is important; It sets a framework for change without interruption
  • Don’t demo until after QA and refactoring – or you’re putting more cats into the wall than you can afford to get out
  • You won’t be able to do it all – be honest about where you’re lacking
  • Option from the crowd – consider a chat room vs. a meeting for daily scrum

Selling Fiddler – Eric Lawrence

  • Have metrics to establish a price
  • Make something, and share it

Developer Flow – Rob Keefer

  • Flow (happiness) comes from a goal, feedback and a balance between challenge and skill
  • Week level – write a plan, have a board to track progress, identify your skill gaps
  • Day level (Pomodoro) – write down task(s), track your progress/interruptions, ensure tasks can be accomplished
  • Moment level (TDD) – write a test, run the test, gain confidence as you pass each piece
  • Shut down e-mail/phone/etc. for as long of stretches as possible (ex: 2hrs)

Long Lived Code Base – Jeremy Miller

  • Architecture can’t be set in stone
  • Have living documentation as part of your build process
  • Diagnostics will be critical over time, invest in them

Growing Beyond Independent – Jay Harris

  • Showed his talk outline as a full screen of icons (no text) then went through each in turn
  • An interview is really about setting expectations, in both directions
  • You have to trust your employees
  • Learn to delegate – not that you can’t do it, it’s that you could be doing something else

C# On Arduino – Eric Potter

  • Scriptcs lets you quickly write and run C# from the command line
  • Chocolatey is an excellent way to install tools on Windows
  • Arduino runs C and is half the cost of Netduino which runs C#
  • Utilizing scriptcs, a library pushed to Arduino and a USB connection Arduino can be made to run C#
  • Want: Build a missile launch type system where two key turns are required to push to production

Summary

CodeMash is an awesome conference that I highly recommend.  Thanks again to all of the speakers, conference organizers, sponsors and my employer Quintiles. 🙂

I have a Visual Studio solution that includes several different potential startup projects, and I find myself frequently switching between them the hard way through the solution explorer.  It is enough of an irritant that I started developing a new capability into CodeMaid to provide a quick way to switch between startup project(s).  I made it about a third of the way in (Card 171) when I discovered something fantastic..

Somebody else had already done it!  Even better than writing code to solve one of your problems is finding someone else who has done it for you! 😉

Behold SwitchStartupProject, a free Visual Studio extension by Andreas Bilger.

How to get it?

Simply search for “SwitchStartupProject” under Visual Studio’s Tools->Extensions and Updates.  Or here’s a direct link.

How does it work?

In the Visual Studio toolbar you’ll see a new drop down that lists the current startup project.

Toolbar

Toolbar

You can open the drop down and see all of the projects in the solution and choose a different startup project.

Dropdown

Dropdown

Well that’s nice, but do I really need to see my unit test projects and other projects that can’t really be started?  Well lets drop down to Configure…

Mode

Mode

Smart mode sounds good, and it does just what you would expect – limits the list to the right kind of project types that can be started.

Dropdown Filtered

Dropdown Filtered

Excellent.  Now I also like to set multiple startup projects at once (e.g. a client project and a server project).  What have you got for me there?  Back to the configuration options where we can setup profiles that contain more than one startup project.

Multiple Projects

Multiple Projects

Now our defined profile(s) will appear as an option in the dropdown menu.

Dropdown Multiple

Dropdown Multiple

How active is it?

Well, he beat me by a week to publishing a VS14 CTP compatible version.. so lets call it very active. 😉  To check out the source and more head to the project home here: https://bitbucket.org/thirteen/switchstartupproject/

Summary

This plugin is exactly what I had in mind to build.  My gratitude to Andreas Bilger for building this excellent little tool so now I don’t have to and I can keep coding away on other features. 🙂  I’ve just started playing with it so if you have any comments or experience with it, please sound off below.

I’ll be attending That Conference this week. If you’re in the Wisconsin Dells neighborhood and want to say hi send me a tweet (@codecadwallader). 🙂