Thursday, May 28, 2009

TomTom sucks for customisation.

I recently emailed TomTom technical support stating that I was a software developer (and TomTom owner) interested in writing software for the TomTom device. I wanted to know where to get the SDK that they did have available up to about 1 year ago but now seems to have disappeared. This was their response:

Dear Mr Clark,

Thank you for your query. Your incident reference number is 090526-001861.

Unfortunately the Software Development Kit is no longer available from TomTom.

Please note that we are unable to offer any technical support for 3rd party add ons or software tools used in conjunction with your TomTom device.

Best Regards,

David A

TomTom Customer Support

Damn that sucks. What is with companies that want to keep you locked down to the software they make on a device that you own. They don’t even sell the navcore, they make nothing from it – they sell the hardware and the maps! It should only increase the value of the device and maps if there is more software available, and the cost to them of releasing their SDK would be minimal. Pretty short sighted if you ask me.

There are lots of little software projects that are now dead in the water due to the lack of an SDK for the latest version.

Tuesday, May 26, 2009

How to make images look disabled (greyed out) when the Button or MenuItem they are within is disabled in WPF

By default in WPF image controls that are within disabled menus and buttons will not be greyed out. One common way to achieve a standard look is to apply a style to the image that will cause it to be partially transparent when its parent is disabled. You create a style with TargetType of Image that is a Setter for the Opacity property with a Style.Trigger bound to the IsEnabled property of the image's parent (via RelativeSource). (Code later...) I made two triggers initially, as some of my Images are in Buttons and some are in MenuItems.

Just looking at my output window, I noticed a whole bunch of errors when the UI was coming up, some like this:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 
'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.MenuItem', 
AncestorLevel='1''. BindingExpression:Path=IsEnabled; DataItem=null; target 
element is 'Image' (Name=''); target property is 'NoTarget' (type 'Object')

And others like this:

System.Windows.Data Error: 4 : Cannot find source for binding with reference 
'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.Button', 
AncestorLevel='1''. BindingExpression:Path=IsEnabled; DataItem=null; target 
element is 'Image' (Name=''); target property is 'NoTarget' (type 'Object') 

Thinking about it for a second, you can see that both of the style triggers will get applied to each image. For each image one will work and one will fail - there can only be one type for the parent element, Button or MenuItem not both. It is a silent failure so could just be lived with but I wanted to fix it.

First I was trying to find a way to choose which trigger to apply depending on the type of the parent*. I know you could remove the TargetType from the style and apply it as a named style to each element, but that just becomes a mess. The whole idea of this solution is to make at as simple to use as possible - basically just letting it modify the behaviour of all images. The real solution of course is to realise that the IsEnabled property is not a member of Button nor MenuItem directly, but it is on the UIElement class.

The fix then is to have only one Style.Trigger with the AncestorType of UIElement:

<Style TargetType="{x:Type Image}">
    <Style.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type UIElement}, AncestorLevel=1}, Path=IsEnabled}" Value="False">
            <Setter Property="Opacity" Value="0.3"></Setter>
        </DataTrigger>
    </Style.Triggers>
</Style> 

* I would still like to know how to do this, but that will have to wait until another time when I really do need to do it. No time!

Programmatically changing TreeViewItem IsSelected fails to set IsFocused

I was having an odd problem with the IsSelected property of TreeViewItems. After adding an item and setting its IsSelected property, the originally selected item is no longer selectable.

I am using the Model-View-ViewModel pattern and a HierarchicalDataTemplate to bind a TreeView. The ItemsSource is an ObservableCollection. I noticed that when I added a new item to the collection (making it IsExpanded and IsSelected) it was added fine and was selected, but the parent item was no longer selectable. Any other item in the TreeView was selectable, and if you select one then the parent was again selectable, but right after the Add on the collection it was broken.

Seems to be a known problem: the IsFocused property is not updated when you change IsSelected via code. I am a little surprised that it is also not kept up to date when you set it via a binding… but I guess in the end binding is just code that you didn’t have to write yourself. Hopefully someone, somewhere will decide that this is indeed a bug so I can remove the event handler that I have put in place as a workaround*

Sample reproduction code:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1;assembly="
    Title="Window1" Height="300" Width="300">
    <Window.CommandBindings>
        <CommandBinding Command="{x:Static local:Window1.AddChildItem}" 
                            CanExecute="AddChildItem_CanExecute"
                            Executed="AddChildItem_Executed" />
    </Window.CommandBindings>
    <Window.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:Item}"
                                  ItemsSource="{Binding Children}">
            <TextBlock>
                <TextBlock Text="Name -> " />
                <TextBlock Text="{Binding ItemName}" />
                <TextBox></TextBox>
            </TextBlock>
        </HierarchicalDataTemplate>
        <Style TargetType="{x:Type TreeViewItem}">
            <!-- property bindings -->
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            <!-- Workaround: Keep the IsFocused property up to date when the IsSelected property changes -->
            <EventSetter Event="Selected" Handler="TreeViewItem_Selected" />
        </Style>
    </Window.Resources>
    <DockPanel LastChildFill="True">
        <ToolBarTray DockPanel.Dock="Top" Height="21">
            <ToolBar>
                <Button Command="{x:Static local:Window1.AddChildItem}">
                    <TextBlock Text="Add" />
                </Button>
            </ToolBar>
        </ToolBarTray>
        <TreeView Name="tv1" ItemsSource="{Binding Items}">
        </TreeView>
    </DockPanel>
</Window>

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
 
namespace WpfApplication1 {
    public partial class Window1 : Window {
        private ObservableCollection<Item> _items = new ObservableCollection<Item>();
        public ObservableCollection<Item> Items {
            get { return _items; }
        }
 
        public static readonly RoutedCommand AddChildItem = new RoutedCommand("AddChildItem", typeof(Window1));
 
        public Window1() {
            InitializeComponent();
 
            _items.Add(new Item { ItemName = "One" });
            _items.Add(new Item { ItemName = "Two" });
            _items.Add(new Item { ItemName = "Three" });
 
            this.DataContext = this;
        }
 
        private void AddChildItem_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
            if(tv1 != null && tv1.SelectedItem != null) {
                e.CanExecute = true;
            }
        }
 
        private void AddChildItem_Executed(object sender, ExecutedRoutedEventArgs e) {
            if(tv1.SelectedItem != null) {
                (tv1.SelectedItem as Item).AddChildItem();
            }
        }
 
        // Without this event handler you will see the problem
        private void TreeViewItem_Selected(object sender, RoutedEventArgs e){
            TreeViewItem tvi = e.OriginalSource as TreeViewItem;
            if(tvi != null && !tvi.IsFocused) {
                tvi.Focus();
            }
        }
    }
 
    public class Item : INotifyPropertyChanged {
        private string _itemName = "New Item";
        public string ItemName {
            get { return _itemName; }
            set { _itemName = value; }
        }
        private ObservableCollection<Item> _children = new ObservableCollection<Item>();
        public ObservableCollection<Item> Children {
            get { return _children; }
        }
        private Item _parent = null;
        public Item Parent {
            get { return _parent; }
        }
 
        public Item() : this(null) { }
        public Item(Item parent) {
            _parent = parent;
        }
 
        public void AddChildItem() {
            Item i = new Item(this);
            _children.Add(i);
            i.IsSelected = true;
        }
 
        private bool _isSelected;
        public bool IsSelected {
            get { return _isSelected; }
            set {
                if(_isSelected != value) {
                    _isSelected = value;
                    OnPropertyChanged("IsSelected");
                }
            }
        }
 
        private bool _isExpanded = true;
        public bool IsExpanded {
            get { return _isExpanded; }
            set {
                if(_isExpanded != value) {
                    _isExpanded = value;
                    if(_isExpanded && _parent != null) {
                        _parent.IsExpanded = true;
                    }
                    OnPropertyChanged("IsExpanded");
                }
            }
        }
 
        #region INotifyPropertyChanged Members
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        #endregion
 
        protected virtual void OnPropertyChanged(string propertyName) {
            if(this.PropertyChanged != null) {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Just Comment out the call to Focus to see the problem.

Of note also is the fact that you must set IsSelected after the new item is inserted into the visual tree. For me that meant a small refactor (seen in the example in AddChildItem) to create the item, add it to the collection then set its IsSelected property rather than creating it with IsSelected already set to true. Otherwise it seems that the call to Focus fails.


* I.e. my code was all nice and separated, I was not handling any TreeView or TreeViewItem events, it was all done with smoke and mirrors** in a layer between the UI and the BO

** Data binding