Saturday 24 December 2011

Obelisk - WP7 MVVM Tombstone Library

Just published first release of "Obelisk" a WP7 library designed to take the pain out of MVVM tombstone persistence.

This library offers an easy way to implement tombstone persistence in WP7 applications. Once the TombstoneHelper is attched to the application, View Models can be registered and all properties with [TombstonePersist] attribute set will be automatically persisted.

Monday 19 December 2011

WP7 Microsoft.Phone.BackgroundAudio.AudioTrack.Tag String Length Limit

This is a bit of an odd one, I found out that the AudioTrack.Tag string property has a limit of 2047, any more than this and an exception is thrown when the AudioTrack object is assigned to BackgroundAudioPlayer.Instance.Track:

HRESULT = 0x8007007A

Friday 16 December 2011

WP7 MVVM Panorama SelectedIndex Binding

The Panorama control doesn't support SelectedItem or SelectedIndex binding which is important for maintaining state on re-activation after tombstoning and may be required for navigation. To solve this problem, I've created a Panorama Behavior called TrackablePonaramaBehavior which can be attached to a panorama and offers a dependency property for binding to the selected index. The System.Windows.Interactivity library needs referencing to use it:

using System;
using Microsoft.Phone.Controls;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace Demo.Behaviors
{
    public class TrackablePanoramaBehavior : Behavior<Panorama>
    {
        private Panorama _panarama = null;
        private bool _updatedFromUI = false;

        // DP for binding index
        public static readonly DependencyProperty SelectedIndexProperty =
            DependencyProperty.Register("SelectedIndex", typeof(int), typeof(TrackablePanoramaBehavior),
            new PropertyMetadata(0, new PropertyChangedCallback(SelectedIndexPropertyChanged)));

        // Index changed by view model
        private static void SelectedIndexPropertyChanged(DependencyObject dpObj, DependencyPropertyChangedEventArgs change)
        {
            if(change.NewValue.GetType() != typeof(int) || dpObj.GetType() != typeof(TrackablePanoramaBehavior))
                return;

            TrackablePanoramaBehavior track = (TrackablePanoramaBehavior)dpObj;

            // If this flag is not checked, the panorama smooth transition is overridden
            if (!track._updatedFromUI)
            {
                Panorama pan = track._panarama;

                int index = (int)change.NewValue;

                if (pan.Items.Count > index)
                {
                    pan.DefaultItem = pan.Items[(int)change.NewValue];
                }
            }

            track._updatedFromUI = false;
        }

        public int SelectedIndex
        {
            get { return (int)GetValue(SelectedIndexProperty); }
            set { SetValue(SelectedIndexProperty, value); }
        }

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

            this._panarama = base.AssociatedObject as Panorama;
            this._panarama.SelectionChanged += _panarama_SelectionChanged;
        }       

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

            if(this._panarama != null)
                this._panarama.SelectionChanged += _panarama_SelectionChanged;
        }

        // Index changed by UI
        private void _panarama_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            _updatedFromUI = true;
            SelectedIndex = _panarama.SelectedIndex;
        }
    }
}


To implement this in the View, the following XAML is used (I chopped most xmlns out to make it more concise):

<phone:PhoneApplicationPage

    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

    DataContext="{Binding Main, Source={StaticResource Locator}}">
  
    <!--LayoutRoot contains the root grid where all other page content is placed-->
    <Grid x:Name="LayoutRoot">
        <controls:Panorama>
            <i:Interaction.Behaviors>
                <track:TrackablePanoramaBehavior SelectedIndex="{Binding Path=SelectedIndex, Mode=TwoWay}" />
            </i:Interaction.Behaviors>


And simply bind to the view model:

public int SelectedIndex
{
    get { return this._selectedIndex; }
    set
    {
        if (this._selectedIndex != value)
        {
            this._selectedIndex = value;

            base.RaisePropertyChanged("SelectedIndex");
        }
    }
}

Monday 12 December 2011

Silverlight (WP7) ListBox & ItemsControl Item Commanding

ListBoxes are a really powerful and flexible way of displaying data in Silverlight, the main problem I find is when you want to use an MVVM pattern and detect when an item is selected and re-selected. It is possible to bind to the SelectedItem property and look for changes in the VM to perform some kind of action, however if you want to select the same item again you need to select something else, then re-select it because SelectedItem becomes latched.

Instead of using a ListBox, it is more flexible to us an ItemsControl (which a ListBox derives from). This has the functionality we require but without the SelectedItem dependency property which we aren't using. The following example has an Items control with a button containing an image in the ItemTemplate:

<Grid x:Name="LayoutRoot">
  <ItemsControl ItemsSource="{Binding Path=Items, Mode=OneWay}">
    <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal" />
      </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <Button>
          <Image Source="{Binding Path=Url, Mode=OneWay}" Stretch="Uniform" />
        </Button>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</Grid>

This is fairly straight forward, but the problem is, if you add a command to the button like this:

<Button Command="{Binding ImageCommand}" CommandParameter="{Binding}" >
  <Image Source="{Binding Path=Url, Mode=OneWay}" Stretch="Uniform" />
</Button>

The button will try and bind to a command in the data item which doesn't exist; instead of the parent view model.

To get round this, the command needs to get outside of the bound item's context, into the main view model by binding to the grid's data context (it could be any named, non-templated element on a page/user control) using the ElementName and Path binding properties:

<Button Command="{Binding ElementName=LayoutRoot, Path=DataContext.ImageCommand}"
  CommandParameter="{Binding}" >
  <Image Source="{Binding Path=Url, Mode=OneWay}" Stretch="Uniform" />
</Button>

This can now easily be bound to a view model containing a command like this:

private ICommand _imageCommand = null;

public ICommand ImageCommand

{
    get { return this._imageCommand; }
}

private bool IsCommandExecutable

{
    get { return true; }
}

// Call this in VM constructor

private void InitialiseCommands()
{
    // Join
    this._imageCommand = new DelegateCommand((param) =>
    {
        this.DoSomething((ImageDetails)param);
        },
            (p) =>
            {
                return this.IsCommandExecutable;
            });
    }

private void DoSomething(ImageDetails obj)

{

}


This technique obviously works with ListBox controls as well, so buttons can be placed alongside other content whilst maintaining the selection mechanism. This example has a delete button alongside some title text:

<Grid x:Name="LayoutRoot">
  <ListBox ItemsSource="{Binding Items, Mode=OneWay}"
    SelectedItem="{Binding SelectedItem, Mode=TwoWay>
    <ListBox.ItemsPanel>
      <ItemsPanelTemplate>
        <StackPanel Orientation="Horizontal" />
      </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <ListBox.ItemTemplate>
      <DataTemplate>
<Grid>
 <Grid.ColumnDefinitions>
   <ColumnDefinition Width="Auto"/>
   <ColumnDefinition Width="*"/>
 <Grid.ColumnDefinitions>
          <TextBlock Text="{Binding Title} />
          <Button Grid.Column="1" Command="{Binding DeleteCommand}" />
        <Grid>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</Grid>

Thursday 8 December 2011

Git .gitignore File and Visual Studio

Just started using Git and GitHub in the last few weeks (missing TFS to be honest!). Anyway it's handy to put a .gitignore file in the root of you repository folder to stop Git showing all the files you don't want to Commit. First off in Windows it's not easy to create a .gitignore file as Windows complains about the file not having a name, so this can be solved by launhing Notepad from a commandline with .gitignore as the file argument:

notepad.exe .gitignore

There is a standard selection of ignore rules on the Help.GitHub site.

In addition to these I added the following for VS:

# VS files
######################
*.suo
*.user
Bin/
obj/
*.
*.vsmdi
*.testsettings
*.cachefile

Wednesday 7 December 2011

Windows Phone Toolkit Expander Template Fix

The expander has a little line which appears alongside the drop-down panel. I noticed that this line is visible behind the header, so I tweaked the template to animate the line opacity so it is hidden when collapsed:

<Style x:Key="ExpanderViewStyle" TargetType="toolkit:ExpanderView">
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <StackPanel/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="toolkit:ExpanderView">
                        <Grid>
                            <Grid.Resources>
                                <QuadraticEase x:Key="QuadraticEaseOut" EasingMode="EaseOut"/>
                                <QuadraticEase x:Key="QuadraticEaseInOut" EasingMode="EaseInOut"/>
                            </Grid.Resources>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="41"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="ExpansionStates">
                                    <VisualStateGroup.Transitions>
                                        <VisualTransition From="Collapsed" GeneratedDuration="0:0:0.15" To="Expanded">
                                            <Storyboard>
                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)" Storyboard.TargetName="ItemsCanvas">
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseOut}" KeyTime="0:0:0.00" Value="0"/>
                                                    <EasingDoubleKeyFrame x:Name="CollapsedToExpandedKeyFrame" EasingFunction="{StaticResource QuadraticEaseOut}" KeyTime="0:0:0.15" Value="1"/>
                                                </DoubleAnimationUsingKeyFrames>
                                                <DoubleAnimation Duration="0" To="1.0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ItemsCanvas"/>
                                                <DoubleAnimation Duration="0" To="1.0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="Line"/>
                                            </Storyboard>
                                        </VisualTransition>
                                        <VisualTransition From="Expanded" GeneratedDuration="0:0:0.15" To="Collapsed">
                                            <Storyboard>
                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(FrameworkElement.Height)" Storyboard.TargetName="ItemsCanvas">
                                                    <EasingDoubleKeyFrame x:Name="ExpandedToCollapsedKeyFrame" EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.00" Value="1"/>
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.15" Value="0"/>
                                                </DoubleAnimationUsingKeyFrames>
                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ItemsCanvas">
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.00" Value="1.0"/>
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.15" Value="0.0"/>
                                                </DoubleAnimationUsingKeyFrames>
                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="Line">
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.00" Value="1.0"/>
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.15" Value="0.0"/>
                                                </DoubleAnimationUsingKeyFrames>
                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="ItemsCanvas">
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.00" Value="0.0"/>
                                                    <EasingDoubleKeyFrame EasingFunction="{StaticResource QuadraticEaseInOut}" KeyTime="0:0:0.15" Value="-35"/>
                                                </DoubleAnimationUsingKeyFrames>
                                            </Storyboard>
                                        </VisualTransition>
                                    </VisualStateGroup.Transitions>
                                    <VisualState x:Name="Collapsed">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="(FrameworkElement.Height)" Storyboard.TargetName="ItemsCanvas"/>
                                            <DoubleAnimation Duration="0" To="0.0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ItemsCanvas"/>
                                            <DoubleAnimation Duration="0" To="0.0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="Line"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Expanded">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetProperty="(FrameworkElement.Height)" Storyboard.TargetName="ItemsCanvas"/>
                                            <DoubleAnimation Duration="0" To="1.0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ItemsCanvas"/>
                                            <DoubleAnimation Duration="0" To="1.0" Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="Line"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="ExpandabilityStates">
                                    <VisualState x:Name="Expandable"/>
                                    <VisualState x:Name="NonExpandable">
                                        <Storyboard>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="ExpandableContent">
                                                <DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="Collapsed"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="Line">
                                                <DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="Collapsed"/>
                                            </ObjectAnimationUsingKeyFrames>
                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="NonExpandableContent">
                                                <DiscreteObjectKeyFrame KeyTime="0:0:0.0" Value="Visible"/>
                                            </ObjectAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <ListBoxItem x:Name="ExpandableContent" Grid.ColumnSpan="2" Grid.Column="0" toolkit:TiltEffect.IsTiltEnabled="True" Grid.Row="0" Grid.RowSpan="2">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="41"/>
                                        <ColumnDefinition Width="*"/>
                                    </Grid.ColumnDefinitions>
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="Auto"/>
                                        <RowDefinition Height="Auto"/>
                                        <RowDefinition Height="Auto"/>
                                    </Grid.RowDefinitions>
                                    <ContentControl x:Name="Header" Grid.ColumnSpan="2" ContentTemplate="{TemplateBinding HeaderTemplate}" Content="{TemplateBinding Header}" Grid.Column="0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Grid.Row="0"/>
                                    <ContentControl x:Name="Expander" ContentTemplate="{TemplateBinding ExpanderTemplate}" Content="{TemplateBinding Expander}" Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Margin="11,0,0,0" Grid.Row="1"/>
                                    <Grid x:Name="ExpanderPanel" Background="Transparent" Grid.ColumnSpan="2" Grid.Column="0" Grid.Row="0" Grid.RowSpan="2"/>
                                </Grid>
                            </ListBoxItem>
                            <Line x:Name="Line" Grid.Column="1" HorizontalAlignment="Left" Grid.Row="2" Opacity="0" Grid.RowSpan="2" Stretch="Fill" Stroke="{StaticResource PhoneSubtleBrush}" StrokeThickness="3" X1="0" X2="0" Y1="0" Y2="1"/>
                            <ContentControl x:Name="NonExpandableContent" Grid.ColumnSpan="2" ContentTemplate="{TemplateBinding NonExpandableHeaderTemplate}" Content="{TemplateBinding NonExpandableHeader}" Grid.Column="0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Grid.Row="0" Grid.RowSpan="2" Visibility="Collapsed"/>
                            <Canvas x:Name="ItemsCanvas" Grid.Column="1" Margin="11,0,0,0" Opacity="0.0" Grid.Row="2">
                                <Canvas.RenderTransform>
                                    <CompositeTransform TranslateY="0.0"/>
                                </Canvas.RenderTransform>
                                <ItemsPresenter x:Name="Presenter"/>
                            </Canvas>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>