Tuesday, December 29, 2009

Twelve Days of Code – Views

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 fourth in this series -- we’ll look at some basic XAML for the View, data binding and some simple styling.

Some basic Layout

Our Pomodoro application doesn’t require a sophisticated layout with complex XAML.  All we really need is an area for our count down timer, and two buttons to start and cancel the Pomodoro.  That being said, we’ll drop a textbox and two buttons into a grid, like so:

<Window x:Class="Pomodoro.Shell.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Pomodoro" Width="250" Height="120">
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        
        <!-- active item -->
        <TextBlock Grid.Column="0"/>
        
        <!-- command buttons -->
        <StackPanel Orientation="Horizontal" Grid.Column="1">
            <Button Content="Start" />
            <Button Content="Stop" />
        </StackPanel>
        
    </Grid>
</Window>

Which looks something likes this:

Basic Pomodo - Yuck!

Binding to the ViewModel

The great thing behind the Model-View-ViewModel pattern is that we don’t need goofy “code-behind” logic to control the View.  Instead, we use WPF’s powerful data binding to graph the View to the ViewModel. 

There are many different ways to bind the ViewModel to the View, but here’s a quick and dirty mechanism until there’s a inversion of control container to resolve the ViewModel:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var sessionController = new TaskSessionController();
        var alarmController = new TaskAlarmController();

        var ctx = new TaskApplicationViewModel(sessionController, alarmController);
        this.DataContext = ctx;
    }
}

The XAML binds to the TaskApplicationViewModel’s ActiveItem, StartCommand and CancelCommands:

<!-- active item -->
<TextBlock Text="{Binding ActiveItem.TimeRemaining}" />

<!-- command buttons -->
<StackPanel Orientation="Horizontal" Grid.Column="1">
    <Button Content="Start" 
            Command="{Binding StartCommand}" 
            CommandParameter="{Binding ActiveItem}"                 
            />
    <Button Content="Stop" 
            Command="{Binding CancelCommand}" 
            CommandParameter="{Binding ActiveItem}"
            />
</StackPanel>

The Count Down Timer

At this point, clicking on the Start button shows the pomodoro counting down in milliseconds.  I had considered breaking the count down timer into its own user control, but chose to be pragmatic and use a simple TextBlock.  The display can be dressed up using a simple converter:

namespace Pomodoro.Shell.Converters
{
    [ValueConversion(typeof(int), typeof(string))]
    public class TimeRemainingConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is int)
            {
                int remaining = (int)value;

                TimeSpan span = TimeSpan.FromMilliseconds(remaining);

                return String.Format("{0:00}:{1:00}", span.Minutes, span.Seconds);
            }
            return String.Empty;
        }

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

Control Button Visibility

With the data binding so far, the Pomodoro application comes to life with a crude but functional user-interface.  To improve the experience, I’d like to make the buttons contextual so that only the appropriate button is shown based on the state of the session.  To achieve this effect, we bind to the IsActive property to control visibility state with mutually exclusive converters: BooleanToVisibleConverter and BooleanToCollapsedConverter.  I honestly don’t know why these converters aren’t part of the framework as I use this concept frequently. 

namespace Pomodoro.Shell.Converters
{
    [ValueConversion(typeof(bool), typeof(Visibility))]
    public class BooleanToHiddenConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is bool)
            {
                bool hidden = (bool)value;
                if (hidden)
                {
                    return Visibility.Collapsed;
                }
            }
            return Visibility.Visible;
        }

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

    [ValueConversion(typeof(bool),typeof(Visibility))]
    public class BooleanToVisibleConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value is bool)
            {
                bool active = (bool)value;

                if (active)
                {
                    return Visibility.Visible;
                }
            }
            return Visibility.Collapsed;
        }

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

However, there’s a Binding problem with all of these converters: by default, the ActiveItem of the TaskApplication is null and none of the bindings will take effect until after the object has been set.  This is easily fixed with a FallbackValue in the binding syntax:

<Window ...
        xmlns:local="clr-namespace:Pomodoro.Shell.Converters"
        xmlns:pc="clr-namespace:System.Windows;assembly=PresentationCore">

    <Window.Resources>
        <local:BooleanToVisibleConverter x:Key="BoolToVisibleConverter" />
        <local:BooleanToHiddenConverter x:Key="BoolToHiddenConverter" />
        <local:TimeRemainingConverter x:Key="TimeSpanConverter" />
    </Window.Resources>

    <Grid>

        <!-- active item -->
        <TextBlock Text="{Binding ActiveItem.TimeRemaining, Converter={StaticResource TimeSpanConverter}, 
                                  FallbackValue='00:00'}" />

        <!-- command buttons -->
        <StackPanel Orientation="Horizontal" Grid.Column="1">
            <Button Content="Start" 
                    Command="{Binding StartCommand}" 
                    CommandParameter="{Binding ActiveItem}"
                    Visibility="{Binding ActiveItem.IsActive, Converter={StaticResource BoolToHiddenConverter}, 
                    FallbackValue={x:Static Member=pc:Visibility.Visible}}"                   
                    />
            <Button Content="Stop" 
                    Command="{Binding CancelCommand}" 
                    CommandParameter="{Binding ActiveItem}"
                    Visibility="{Binding ActiveItem.IsActive, Converter={StaticResource BoolToVisibleConverter}, 
                    FallbackValue={x:Static Member=pc:Visibility.Collapsed}}"
                    />
        </StackPanel>

    </Grid>
</Window>

A few extra applied stylings, and the Pomodoro app is shaping up nicely:

Not so bad Pomodoro

Next Steps

The next post, I’ll look at some of the new Windows 7 integration features available in WPF 4.0.

1 comment:

  1. Hy,
    i think ,making a cool Pomodoro application in.net, is a really great idea.

    Have u ever considered to make this an open source project?
    I would be glad to help you if you need.
    If you are already done with this application please make it free!
    i would use on daily basis

    ReplyDelete