Feb 272010

Download LINQ.psm1

I’m gonna start off by saying the module I’m posting about does not follow the PowerShell naming guidelines. In fact, not only does it use non-standard verb names, but the "verb" part of all the function names aren’t even verbs. I hate breaking the rules like this but the Verb-Noun naming couldn’t really group these functions together the way I wanted and naming them correctly would have been very difficult. So feel free to rename as needed.

A while back I posted about a ConvertTo-Dictionary PowerShell function. This handy little function would take an input object from the pipeline and populate a Hashtable using a key selector and optional value selector. Very similar to how LINQ’s ToDictionary method works. It uses ScriptBlocks in the same way C# uses lambda expressions. There is one very nice trick in the module. A function called Invoke-ScriptBlock that shows how to inject a $_ (dollar underbar) variable into a ScriptBlock’s scope when invoking it. This is very handy for simulating lambdas in PowerShell functions.

Anyhow, over time I added several more LINQ-like functions and then I just went all out and tried to implement all the most popular LINQ methods. I managed to get a good deal done although there is still some documentation work needed. Some of the functions may seem redundant because they have nearly the same functionality as existing PowerShell cmdlets. In these cases it’s usually because I thought it could be more LINQ-like. For example I have Linq-Min because it’s simpler and more flexible than Measure-Object. But I didn’t duplicate Where-Object because there was no point.

The following code example shows how you might use these functions. Not all of them are shown. Notice the lamba-like syntax for predicates, selectors, etc. They also make use of the convenient $_ (dollar underbar) variable to represent the "current item" which makes them more consistent with Where-Object, Foreach-Object, etc.

Import-Module LINQ

function Assert-AreEqual($Expected, $Actual) {
    if (@(Compare-Object $Expected $Actual -SyncWindow 0).Length) {
        $OFS = ','
        Write-Error "Assert-AreEqual Failed: Expected=($Expected), Actual=($Actual)"
    }
}

# All
Assert-AreEqual -Expected ($true)   -Actual (1..5 | Linq-All { $_ -gt 0 })
Assert-AreEqual -Expected ($false)  -Actual (1..5 | Linq-All { $_ -gt 6 })
Assert-AreEqual -Expected ($true)   -Actual (@()  | Linq-All { $_ -gt 6 })

# Any
Assert-AreEqual -Expected ($true)   -Actual (1..5 | Linq-Any)
Assert-AreEqual -Expected ($false)  -Actual (@()  | Linq-Any)
Assert-AreEqual -Expected ($true)   -Actual (1..5 | Linq-Any { $_ -eq 3 })
Assert-AreEqual -Expected ($false)  -Actual (1..5 | Linq-Any { $_ -eq 6 })

# First
Assert-AreEqual -Expected @(1)      -Actual @(1..5 | Linq-First)
Assert-AreEqual -Expected @(2)      -Actual @(1..5 | Linq-First { $_ -gt 1 })
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-First { $_ -gt 5 })

# Last
Assert-AreEqual -Expected @(5)      -Actual @(1..5 | Linq-Last)
Assert-AreEqual -Expected @(4)      -Actual @(1..5 | Linq-Last { $_ -lt 5 })
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Last { $_ -lt 1 })

# Single
Assert-AreEqual -Expected @(2)      -Actual @(1..5 | Linq-Single { $_ -eq 2 })
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Single { $_ -gt 5 })

# Skip
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Skip 0)
Assert-AreEqual -Expected @(3..5)   -Actual @(1..5 | Linq-Skip 2)
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Skip 5)
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Skip 6)

# SkipWhile
Assert-AreEqual -Expected @(2..5)   -Actual @(1..5 | Linq-SkipWhile { $_ -eq 1})
Assert-AreEqual -Expected @(3..5)   -Actual @(1..5 | Linq-SkipWhile { $_ -lt 3})
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-SkipWhile { $false })
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-SkipWhile { $true })

# Take
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Take 5)
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Take 6)
Assert-AreEqual -Expected @(1..5)   -Actual @(1..6 | Linq-Take 5)
Assert-AreEqual -Expected @(1..3)   -Actual @(1..5 | Linq-Take 3)
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Take 0)

# TakeWhile
Assert-AreEqual -Expected @(1..2)   -Actual @(1..5 | Linq-TakeWhile { $_ -lt 3})
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-TakeWhile { $_ -eq 2})
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-TakeWhile { $false })
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-TakeWhile { $true })

# Distinct
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Distinct)
Assert-AreEqual -Expected @(1..5)   -Actual @(1,2,3,4,5,1,2,3,4,5 | Linq-Distinct)
Assert-AreEqual -Expected @()       -Actual @(Linq-Distinct)

# Repeat
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Repeat 1)
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Repeat 0)
Assert-AreEqual -Expected @(1,1,2,2,3,3,4,4,5,5) -Actual @(1..5 | Linq-Repeat 2)

# Except
Assert-AreEqual -Expected @(1,3,5)  -Actual @(1..5 | Linq-Except (2,4))
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Except 6)
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Except (1..5))

# Intersect
Assert-AreEqual -Expected @(2,4)    -Actual @(1..5 | Linq-Intersect (2,4))
Assert-AreEqual -Expected @()       -Actual @(1..5 | Linq-Intersect 6)
Assert-AreEqual -Expected @(1..5)   -Actual @(1..5 | Linq-Intersect (1..5))

# IndexOf
Assert-AreEqual -Expected (0)       -Actual (1..5 | Linq-IndexOf { $_ -eq 1 })
Assert-AreEqual -Expected (3)       -Actual (1..5 | Linq-IndexOf { $_ -eq 4 })
Assert-AreEqual -Expected (-1)      -Actual (1..5 | Linq-IndexOf { $_ -eq 6 })
Assert-AreEqual -Expected (2)       -Actual (1,2,3,3,3 | Linq-IndexOf { $_ -eq 3 })

# Count
Assert-AreEqual -Expected (5)       -Actual (1..5 | Linq-Count)
Assert-AreEqual -Expected (1)       -Actual (1..5 | Linq-Count { $_ -eq 3 })
Assert-AreEqual -Expected (0)       -Actual (1..5 | Linq-Count { $_ -eq 6 })

# Average, Min, Max
Assert-AreEqual -Expected (3)       -Actual (1..5 | Linq-Average)
Assert-AreEqual -Expected (15)      -Actual (1..5 | Linq-Sum)
Assert-AreEqual -Expected (5)       -Actual (1..5 | Linq-Max)
Assert-AreEqual -Expected (1)       -Actual (1..5 | Linq-Min)

The complete function listing is shown below. I’ve been selfishly keeping this module to myself for about a year now and I felt it was time to post it. Let me know what you think!

Name Synopsis
Linq-All Determines whether all elements of a sequence satisfy a condition.
Linq-Any Determines whether any element of a sequence satisfies a condition.
Linq-Average Returns the average of values in a sequence.
Linq-Count Returns a number that represents how many elements in the specified sequence satisfy a condition.
Linq-Distinct Returns unique items from the pipeline input.
Linq-Except Excludes from the pipeline input the items which also appear in a second set.
Linq-Expand Drills into an input object based on one or more property names to simplify access to data inside nested structures.
Linq-First Returns the first element in a sequence that satisfies a specified condition.
Linq-IndexOf Returns the zero-based position of the first element in a sequence that meets the specified criteria.
Linq-Intersect Includes from the pipeline input only the items that exist in a second set.
Linq-Last Returns the last element of a sequence that satisfies a specified condition.
Linq-Max Returns the maximum value in a sequence.
Linq-Min Returns the minimum value in a sequence.
Linq-Repeat Repeats the items from the pipeline a specified number of times.
Linq-Select Selects a property, property set, or ScriptBlock projection of the input.
Linq-SelectMany Similar to Linq-Select but always ensures the output is wrapped in an array.
Linq-Single Returns a single item that satisfies a specified condition, and throws an exception if more than one or zero such element exists.
Linq-Skip Skips the specified number of items from the input and then returns the remaining items.
Linq-SkipWhile Skips items from the input as long as a specified condition is true and then returns the remaining items.
Linq-Sum Returns the sum of values in a sequence.
Linq-Take Returns a specified number of contiguous items from the start of the input pipeline.
Linq-TakeWhile Returns items from the input as long as a specified condition is true.
Linq-ToDictionary Creates a Hashtable from a sequence according to specified key selector and value selector functions.
Linq-ToSet Creates a HashSet containing only unique items from a sequence.
Where-Like Returns items from a sequence whose selected values match one or more wildcard patterns.
Where-Match Returns items from a sequence whose selected values match one or more regular expression patterns.

Download LINQ.psm1

I recently blogged about a new method I noticed in .NET 4 on the Enum class. One of the great things about .NET 4 is that it is a new version of base class library too. In .NET 3.0 and 3.5 as we all know, the CLR and BCL was left at version 2.0 which meant there were practically no improvements to core system classes and such.

Well I just stumbled upon another welcome upgrade to a rusty old class. Path.Combine in .NET 4 takes a parameter array of parts.

// so code that used to look like this...
string documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filename =
    Path.Combine(documents,
        Path.Combine("Visual Studio 2008",
            Path.Combine("Projects", "My Project");

// now looks like this...
string documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string filename = Path.Combine(documents, "Visual Studio 2008", "Projects", "MyProjects");

H3Viewer

Update: There was a bug in H3Viewer at the time I posted this that showed an empty TOC and Index on x64 versions of Windows. Rob Chandler just informed me the current build (1.0.0.20) fixes this and I have confirmed it now works fine on x64! Apparently in between Beta 2 and RC Microsoft may have changed the help agent from 32 bit to 64 bit which relocated some registry keys. H3Viewer is still a 32 bit application but works fine on 64 bit Windows.

I hate Visual Studio 2010’s new help system. Well hate isn’t really the right word. It’s more like loathing with every fiber of my being. There was a pretty good discussion going on in the comments of Brian Harry’s blog on MSDN. The bottom line is though, it’s here to stay.

Fortunately a Microsoft Help MVP named Robert Chandler has created H3Viewer which is an interface on top of the new help system that aims to bring back the familiar feel of dexplore, including a TOC, index, and bookmarks. It doesn’t change the fact that help is now served by a local web server but it’s a great improvement over the out of box experience. Sorry but IE/Chrome are not good help viewers.

Download FullScreenDemo Project

So now I’m working on an application that will be used at a trade show in a booth. Customers will come up to the application and type in the area code and exchange of their phone number and the application will tell them whether or not they can move their phone number over to us. Piece of cake. But obviously a battleship gray Windows Forms application will simply not do. I chose WPF for the project because it needs to access an offline database, interact with an external COM server (MapPoint), and run full screen with keyboard input.

That last point is what led me to the code I posted tonight. Running a WPF application in full screen mode is pretty trivial.

  • Set WindowStyle = None
  • Set TopMost = True
  • Set WindowState = Maximized

But coming off a recent Silverlight project, I was a little perturbed about the fact that Silverlight can easily “go full screen” and back whereas in WPF it felt really manual. Plus there were a few other improvements I wanted to make that I could easily wrap up into a reusable behavior. For one, using a full-screen window during debugging is a pain in the ass, even with two monitors. Therefore, I want to be able to toggle full screen mode at runtime.

Here are a few common ways that applications enter and exit full screen mode. This behavior supports them all.

  • Many full-screen apps allow the user to toggle full screen mode by double clicking inside the window.
  • Many full-screen apps allow the user to exit full screen mode by pressing escape.
  • Many full-screen apps implicitly enter full screen mode when the window is maximized.

The only one that was tricky was the last one. Since WPF does not have a “StateChanging” event, what was happening was the Window (with its border and title bar) is maximized before the event is raised. By that time, setting the WindowStyle property to remove the caption and border didn’t affect the maximized size so the maximized window was still showing the taskbar. It took a little trial and error but I settled on using WM_SYSCOMMAND.

Usage of the behavior is simple.

<Window ... >

    <i:Interaction.Behaviors>

        <Einstein:FullScreenBehavior
                FullScreenOnDoubleClick="True"
                FullScreenOnMaximize="True"
                RestoreOnEscape="True" />

    </i:Interaction.Behaviors>

</Window>

The attached demo includes the FullScreenBehavior.cs which has no external dependencies. Enjoy.

Full Screen Demo

Shh… hear that? It’s the sound of a million developers ripping out their home-grown HasFlag, IsFlagSet, CheckFlag, etc helper methods. Just noticed that in .NET 4 System.Enum now has a built-in HasFlag method. As Forrest Gump would say “Lt. Dan says we don’t have to worry about enum flags no more. That’s good. One less thing.”

I like unit testing. I’m not a hard liner so I don’t take it too seriously. I don’t necessarily fully subscribe to the test-first approach in all cases and I think aiming for 100% code coverage is rarely practical. But I do like unit tests. They give my a certain peace of mind and set a minimum standard for functionality.

But I just ran into a bug in my code that I didn’t catch in my unit tests but the end user caught pretty early on. Typing a decimal value into a field that was bound to an Int64 property on the viewmodel didn’t produce any error but simply reverted the field to zero. As the developer, it never occurred to me that someone would want to put a non-integer value in the field. I even guarded against negative integers.

Anyhow, the fix for now is “don’t type fractional minutes in the minutes field.” The bug is not particularly disruptive, just confusing. The nature of the field is such that a fractional minute is meaningless, but the end user is entering numbers directly off a bill which happens to have way more precision than is needed. It’s super low priority so I’ll queue it up for when other bugs need to be fixed. But I thought it was a great example of how developers can know the code inside and out, yet the end user will always find a bug you never even considered.

It’s a simple one, but useful. And it can clean up some of your ugly scripts that have a lot of redundant paths embedded in strings.

# Reduced for the sake of the example
# The full advanced function is included at the end.
function Use-Location($Path, $Body) {

    Push-Location $Path

    try { &$Body }
    finally { Pop-Location }

}

Usage of the function couldn’t be simpler and uses a C#-style scope syntax.

Use-Location "C:\Temp" {

    mkdir textfiles
    dir *.txt | move textfiles

    mkdir images
    dir *.jpg,*.png | move images

}

Obviously you could just as well use Push-Location and Pop-Location in a try/catch yourself but why? This way enforces clean scoping and ensures you don’t forget the call to pop.

##############################################################################
#.SYNOPSIS
# Executes a ScriptBlock within the context of the specified working directory.
#
#.DESCRIPTION
# This function sets the current location to the specified Path and executes a
# ScriptBlock. All relative paths in the ScriptBlock will be based on the new
# current directory. When the ScriptBlock exits, the old current directory
# will be restored.
#
#.PARAMETER Path
# The working directory to use for the duration of the ScriptBlock.
#
#.PARAMETER ScriptBlock
# The code to execute within the context of the new current directory.
#
#.PARAMETER Create
# Creates the directory if it does not exist.
#
#.EXAMPLE
# Use-Location "C:\Temp" {
#
#    mkdir textfiles
#    dir *.txt | move textfiles
#
#    mkdir images
#    dir *.jpg,*.png | move images
#
# }
#
#.LINK 
# Push-Location
# Pop-Location
##############################################################################
function Use-Location {

    [CmdletBinding()]
    param(
        
        [Alias('PSPath')]
        [Alias('LiteralPath')]
        [Parameter(Position=1, Mandatory=$true)]
        [String]$Path,
        
        [Parameter(Position=2, Mandatory=$true)]
        [ScriptBlock]$ScriptBlock,
        
        [Parameter()]
        [Switch]$Create
        
    )

    $ErrorActionPreference = 'Stop'

    # Create if necessary
    if ($Create) {
        $Exists = Test-Path -LiteralPath $Path -PathType Container
        if (-not $Exists) {
            New-Item $Path -ItemType Directory | Out-Null
        }
    }

    Push-Location $Path -StackName 'Use-Location'

    try { &$ScriptBlock }
    finally { Pop-Location -StackName 'Use-Location' }

}

Download Animated Number Project

If you’ve used Turbo Tax, you may have noticed the prominent “Federal Refund” box that’s ever-present at the top of the page while you’re figuring out your taxes. I like this UI concept because when most people are filing their taxes, there’s only one number they actually care about and they care about it every step of the way.

One nice little touch about the display is that whenever you make changes to your return that affect the federal refund, instead of the field just changing immediately, it has a nice incrementing (or decrementing if you’re like me) animation. It is a subtle effect that draws your attention to the field whenever it changes. Try the working example below.

Recently I worked on a line of business application that would benefit from a similar UI. The application is for pricing out sales proposals and takes into account cost of goods, retail price, agent commission, etc. At every step of the way, slight changes to the various inputs affect the bottom line which is the profitability of the deal. So the profitability is always displayed on the screen. When the value changes, I wanted to have this smooth animation between numbers just like Turbo Tax. Turns out it isn’t that hard.

Before we begin

I just want to note that if I just wanted to create a one-off solution, it could probably be done in about 10 lines of code with event handlers. The purpose of this article is to create a control that is effortless to use and re-use. If you want, you can just skip to the sample project and look at the code. It’s really not that complex.

Creating the Control

The idea is very simple. A control that lets you bind a numeric value to it and when that value changes, the displayed number should quickly increment or decrement to the new value instead of changing immediately. The example above shows the finished control in action.

At first I didn’t want to do this as a control. I thought maybe there was some way I could do it at the binding level or with an attached property but that didn’t seem to be possible via any of the extensibility points offered by Silverlight. So I decided to make it a control.

What kind of control should it be? Should it derive from ContentControl? TextBlock? Neither of those seemed appropriate because in order to make it work, I would need a strongly typed Value property. Having a simple Content property based on System.Object would imply that it could accept any type. I didn’t want to go crazy supporting different types. For example, there’s no point in making it able to animate between two DateTime values or TimeSpan. I decided to stick with System.Double as the type of the Value property. Any numeric type would be convertible to/from Double and there’s a built in DoubleAnimation that I could take advantage of.

I settled on creating a new class called AnimatedNumber that derives from Control (not ContentControl.) I added two dependency properties:

public class AnimatedNumber : Control {
    // the actual code defines theese as dependency properties
    public double Value { get; set; }
    private double AnimatedValue { get; set; }
}

The idea here is that the Value property is the bindable public property that would be set to a discrete value such as 0, 100, 200, etc. When this happens we will animate the private AnimatedValue property so that over a short period of time, the AnimatedValue property will quickly change from 0, 1, 2, … 100, … 198, 199, 200.

To do this, I included a PropertyChangedCallback with the ValueProperty registration. This callback is invoked whenever the Value property is changed (either via x.Value = 100 or as a result of a binding expression.) In the callback, we’ll run a Storyboard against the AnimatedValue property that uses a DoubleAnimation to interpolate between the old and new values.

private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    var senderControl = (AnimatedNumber)sender;
    senderControl.AnimateValue((double)e.NewValue);
}

private void AnimateValue(double newValue)
{
    var animation = new DoubleAnimation();
    animation.Duration = TimeSpan.FromSeconds(1);
    animation.To = newValue;

    var storyboard = new Storyboard();
    storyboard.Duration = TimeSpan.FromSeconds(1);
    storyboard.Children.Add(animation);

    Storyboard.SetTarget(animation, this);
    Storyboard.SetTargetProperty(animation, new PropertyPath("AnimatedValue"));

    storyboard.Begin();
}

So now we have a control that exposes a Value that when changed, triggers an incremental change to the new value in a corresponding AnimatedValue property. But so far, the control has absolutely no visual representation! We’ve only defined the behavior of the control.

Creating a Default Control Template

To add a default control template, add a folder named “Themes” to the root of the project and create a resource dictionary named Generic.xaml. Add a style with no key and a setter that defines a very basic control template. In fact, the only thing this control has is a ContentPresenter that will present the AnimatedValue property. The entire Generic.xaml is only a few lines long. The control doesn’t need anything more than that because we eventually want to use it wherever we’d place “content” such as in a button or label. Let some other control provide the eye candy.

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:Local="clr-namespace:AnimatedNumberDemo">
  <Style TargetType="Local:AnimatedNumber">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="Local:AnimatedNumber">
          <ContentPresenter x:Name="PART_Content" />
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
</ResourceDictionary>

The important thing is that we have a ContentPresenter named (exactly) PART_Content. You’ll see why in a second.

To make Silverlight associate the above template with our control, we need to add a line to the constructor, and while we’re at it, override OnApplyTemplate. This is the best place to grab the template part we expect to find (PART_Content) and bind it to the AnimatedValue property.

// Template part
private ContentPresenter PART_Content;

// Default public constructor
public AnimatedNumber() {
    DefaultStyleKey = typeof(AnimatedNumber);
}

protected override void OnApplyTemplate() {

    base.OnApplyTemplate();

    PART_Content = GetTemplateChild("PART_Content") as ContentPresenter;
    if (PART_Content != null) {
        var binding = new Binding("AnimatedValue");
        binding.Source = this;
        PART_Content.SetBinding(ContentPresenter.ContentProperty, binding);
    }

}

Okay, now in theory it should work but I would advise reading the next section first and looking at the *actual* code I attached as opposed to the select snippets I put inline.

Okay, now it should work…

Really this is all that is needed to make the control functional. But it’s a little crude. The attached sample project is a bit more cleaned up than the inline code so far. There’s also additional functionality.

For one, the code above would use the default Double.ToString() conversion which means as the animation was in progress, you’d probably see a huge ugly number with lots of decimal places, no separators, and no currency symbols. The attached control adds a Format property and uses an included FormattingValueConverter. This way you can specify a format string as an attribute in the XAML to get the nice formatting as seen in the demo at the beginning of the article.

Also, the attached control does not create a new Storyboard everytime the value is animated. A single Storyboard is created in the constructor and is reused for the life of the control. The nice thing about this is that multiple changes to the value will “interrupt” the previous animation. So for example if it’s halfway through animation from 0 to 100 and you change it back to 0, it won’t continue to climb to 100 then drop to 0. It will stop climbing at 50 or whatever, and start dropping.

Finally, the attached control provides a TransitionDuration property that lets you control how long the control will spend transitioning. Right now the time is constant which is a little weird.

Using the code is pretty straightforward. The following is an example of a simple button that is bound to a TotalProfit value of the ViewModel and animates the value when it changes.

<Button Style="{StaticResource GlassButton}">
    <StackPanel>
        <TextBlock Text="Total Profit" />
        <Local:AnimatedNumber Value="{Binding TotalProfit}" Format="C2" />
    </StackPanel>
</Button>

Ideas for Additional Improvements

Some things that I didn’t have time to get to but would be nice:

  • Using an easing function to make the animation more fluid
  • Varying the duration of the animation based on how large or small the change is. For example if you changed 1000 to 1009, it’s kind of silly that the control spends 2 seconds making that transition.
  • Adding support for the Visual State Manager. In particular, it would be nice to have 3 states – Positive, Zero, Negative. This way you could color the value red when negative and green when positive without putting too much burden on the consumer of the control.

Please let me know what you think of this example. It’s my first time posting a lenthy example of a Silverlight control but as time permits I’d like to share more of my recent experiences.

Download Animated Number Project

And hopefully many more will follow suit. SharePoint 2010 also does not support IE6. Hopefully this will hasten it’s demise.

Dear Google Apps admin,​

In order to continue to improve our products and deliver more sophisticated features and performance, we are harnessing some of the latest improvements in web browser technology.  This includes faster JavaScript processing and new standards like HTML5.  As a result, over the course of 2010, we will be phasing out support for Microsoft Internet Explorer 6.0 as well as other older browsers that are not supported by their own manufacturers.

We plan to begin phasing out support of these older browsers on the Google Docs suite and the Google Sites editor on March 1, 2010.  After that point, certain functionality within these applications may have higher latency and may not work correctly in these older browsers. Later in 2010, we will start to phase out support for these browsers for Google Mail and Google Calendar.

Google Apps will continue to support Internet Explorer 7.0 and above, Firefox 3.0 and above, Google Chrome 4.0 and above, and Safari 3.0 and above.

Starting this week, users on these older browsers will see a message in Google Docs and the Google Sites editor explaining this change and asking them to upgrade their browser.  We will also alert you again closer to March 1 to remind you of this change.

In 2009, the Google Apps team delivered more than 100 improvements to enhance your product experience. We are aiming to beat that in 2010 and continue to deliver the best and most innovative collaboration products for businesses.

Thank you for your continued support!

Sincerely,

The Google Apps team

Email preferences: You have received this mandatory email service announcement to update you about important changes to your Google Apps product or account.

Google Inc.
1600 Amphitheatre Parkway
Mountain View, CA 94043