Thursday, December 31, 2009

Twelve Days of Code – Windows 7 Shell Integration

As part of the twelve days of code, I’m building a Pomodoro style task tracking application and blogging about it. This post is the fifth in this series.  Today I’ll cover adding some of the cool new Windows 7 Shell integration features in WPF 4.0.

I’ll be revisiting some of the xaml and object model for this post, so it wouldn’t hurt to read up:

WPF 4.0 offers some nice Windows 7 Shell integration that are easy to add to your application.

Progress State

Since the Pomdoro application’s primary function is to provide a countdown timer, it’s seems like a natural fit to use the built-in Windows 7 Taskbar progress indicator to show the time remaining.  Hooking it up was a snap.  The progress indicator uses two values ProgressState and ProgressValue, where ProgressState indicates the progress mode (Error, Indeterminate, None, Normal, Paused) and ProgressValue is a numeric value between 0 and 1.  Two simple converters provide the translation between ViewModel and View, one to control ProgressState and the other to compute ProgressValue.

<Window.TaskbarItemInfo>
    <TaskbarItemInfo
        ProgressState="{Binding ActiveItem, Converter={StaticResource ProgressStateConverter}}"
        ProgressValue="{Binding ActiveItem, Converter={StaticResource ProgressValueConverter}}"
        >
    </TaskbarItemInfo>
</Window.TaskbarItemInfo>

Which looks something like this:

taskbar-progress

Note that for ProgressValue I’m binding to ActiveItem instead of TimeRemaining.  This is because the progress value is obtained through a percent complete calculation -- time remaining in the original session length – which requires that both values are available to the converter.  I suppose this could have been calculated through a multi-binding, but the single converter makes things much easier.

namespace Pomodoro.Shell.Converters
{
    [ValueConversion(typeof(ITaskSession), typeof(System.Windows.Shell.TaskbarItemProgressState))]
    public class ProgressStateConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value != null && value is ITaskSession)
            {
                ITaskSession session = (ITaskSession)value;

                if (session.IsActive)
                {
                    return TaskbarItemProgressState.Normal;
                }
            }
            return TaskbarItemProgressState.None;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }


    [ValueConversion(typeof(ITaskSession), typeof(double))]
    public class ProgressValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value != null && value is ITaskSession)
            {
                ITaskSession session = (ITaskSession)value;

                if (session.IsActive)
                {
                    int delta = session.SessionLength - session.TimeRemaining;
                    
                    return (double)(delta / (double)session.SessionLength);
                }
            }
            return 1;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

This is all well and good, but there’s a minor setback: Progress will not increment unless the PropertyChanged event is raised for the ActiveItem property.  This is an easy fix: the ITaskSession needs to expose a NotifyProgress event and the ITaskApplication needs to notify the View when the event fires.  Since the progress indicator in the taskbar is only a few dozen pixels wide, spamming the View with each millisecond update is a bit much.  We solve this problem by throttling the amount the event is raised using a NotifyInterval property.

// from TaskSessionViewModel
void OnTimer(object sender, ElapsedEventArgs e)
{
    NotifyProperty("TimeRemaining");

    if (IsActive)
    {
        // if notify interval is set
        if (NotifyInterval > 0)
        {
            notifyIntervalCounter += TimerInterval;

            if (notifyIntervalCounter >= NotifyInterval)
            {
                if (NotifyProgress != null)
                {
                    NotifyProgress(this, EventArgs.Empty);
                }
                notifyIntervalCounter = 0;
            }
        }
    }

    if (TimeRemaining == 0)
    {
        // end timer
        IsActive = false;

        if (SessionFinished != null)
        {
            SessionFinished(this, EventArgs.Empty);
        }
    }
}

TaskbarInfo Buttons

Using the exact same command bindings mentioned in my last post, adding buttons to control the countdown timer from the AreoPeek window is dirt simple. The only gotcha is that the taskbar buttons do not support custom content, instead you must specify an image to display anything meaningful to the user.

<Window ....>

    <Window.Resources>
        <!-- play icon for taskbar button -->
        <DrawingImage x:Key="PlayImage">
            <DrawingImage.Drawing>
                <DrawingGroup>
                    <DrawingGroup.Children>
                        <GeometryDrawing Brush="Black" Geometry="F1 M 50,25L 0,0L 0,50L 50,25 Z "/>
                    </DrawingGroup.Children>
                </DrawingGroup>
            </DrawingImage.Drawing>
        </DrawingImage>

        <!-- stop icon for taskbar button -->
        <DrawingImage x:Key="StopImage">
            <DrawingImage.Drawing>
                <DrawingGroup>
                    <DrawingGroup.Children>
                        <GeometryDrawing Brush="Black" Geometry="F1 M 0,0L 50,0L 50,50L 0,50L 0,0 Z "/>
                    </DrawingGroup.Children>
                </DrawingGroup>
            </DrawingImage.Drawing>
        </DrawingImage>

        // ... converters
        
    </Window.Resources>

    <Window.TaskbarItemInfo>
        <TaskbarItemInfo ... >
            
            <TaskbarItemInfo.ThumbButtonInfos>
                <ThumbButtonInfoCollection>

                    <!-- start button -->
                    <ThumbButtonInfo
                        ImageSource="{StaticResource ResourceKey=PlayImage}"
                        Command="{Binding StartCommand}"
                        CommandParameter="{Binding ActiveItem}"
                        Visibility="{Binding ActiveItem.IsActive, 
                                     Converter={StaticResource BoolToHiddenConverter}, 
                                     FallbackValue={x:Static Member=pc:Visibility.Visible}}"
                        />

                    <!-- stop button -->
                    <ThumbButtonInfo
                        ImageSource="{StaticResource ResourceKey=StopImage}"
                        Command="{Binding CancelCommand}"
                        CommandParameter="{Binding ActiveItem}"
                        Visibility="{Binding ActiveItem.IsActive, 
                                     Converter={StaticResource BoolToVisibleConverter}, 
                                     FallbackValue={x:Static Member=pc:Visibility.Collapsed}}"
                        />
                </ThumbButtonInfoCollection>
            </TaskbarItemInfo.ThumbButtonInfos>
        </TaskbarItemInfo>
    </Window.TaskbarItemInfo>

    // ....

</Window>

The applied XAML looks like this:

taskbar-buttons

Next Steps

The next post we’ll add some auditing capability to the pomodoro application using SQLite and the Entity Framework version in .NET 4.0.

submit to reddit

0 comments: