TabControl’s ContentTemplate cannot be changed dynamically

TabControl bug exampleI recently used a TabControl and required a different ContentTemplate per item, so I used datatriggers to change the ContentTemplate based on the current selected page. In theory this should work fine, but I found that changes to the ContentTemplate are not effectuated.

How to reproduce?
Use the following XAML code to illustrate the problem. The property trigger based on the ‘IsMouseOver’ property changes the content template, when you hover over it. This should make the text to be displayed in bold, but it doesn’t show until you change the page.

<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

  <Page.Resources>

    <DataTemplate x:Key="TabHeaderTemplate">
      <TextBlock Text="{Binding XPath=@header}"/>
    </DataTemplate>

    <DataTemplate x:Key="TabContent1">
      <TextBlock FontWeight="Normal" Text="{Binding XPath=@header}"/>
    </DataTemplate>

    <DataTemplate x:Key="TabContent2">
      <TextBlock FontWeight="Bold" Text="{Binding XPath=@header}"/>
    </DataTemplate>

    <Style x:Key="DynamicTabControl" TargetType="TabControl">
      <Setter Property="ContentTemplate" Value="{StaticResource TabContent1}"/>
      <Style.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
          <Setter Property="ContentTemplate" Value="{StaticResource TabContent2}"/>
        </Trigger>
      </Style.Triggers>
    </Style>

    <XmlDataProvider x:Key="tabPages" XPath="/pages">
      <x:XData>
        <pages xmlns="">
          <page header="Page 1"/>
          <page header="Page 2"/>
          <page header="Page 3"/>
        </pages>
      </x:XData>
    </XmlDataProvider>

  </Page.Resources>

  <TabControl ItemsSource="{Binding Source={StaticResource tabPages}, XPath=*}"
              ItemTemplate="{StaticResource TabHeaderTemplate}"
              Style="{StaticResource DynamicTabControl}"/>

</Page>

What’s the cause?
I was wondering what was happening, so I started my favorite .NET Reflector tool and found out that the ContentTemplate is set in code instead of a binding. The following piece of code takes care of setting the content template and horizontal/vertical alignment (which will probably suffer from the same problem):

private void TabControl.UpdateSelectedContent()
{
  // Check if a tab is selected
  if (base.SelectedIndex < 0)
  {
    this.SelectedContent = null;
    this.SelectedContentTemplate = null;
    this.SelectedContentTemplateSelector = null;
    this.SelectedContentStringFormat = null;
  }
  else
  {
    // Obtain the selected tab item
    TabItem selectedTabItem = this.GetSelectedTabItem();
    if (selectedTabItem != null)
    {
      // Some keyboard stuff removed here...

      // Set the content
      this.SelectedContent = selectedTabItem.Content;

      // Obtain the content presenter for the selected item
      ContentPresenter selectedContentPresenter = this.SelectedContentPresenter;
      if (selectedContentPresenter != null)
      {
        // Set horizontal/vertical alignment
        selectedContentPresenter.HorizontalAlignment = selectedTabItem.HorizontalContentAlignment;
        selectedContentPresenter.VerticalAlignment = selectedTabItem.VerticalContentAlignment;
      }

      // Check if the selected tab item defines the content template
      if ((selectedTabItem.ContentTemplate != null) ||
          (selectedTabItem.ContentTemplateSelector != null) ||
          (selectedTabItem.ContentStringFormat != null))
      {
        // Use the content settings from the tab item
        this.SelectedContentTemplate = selectedTabItem.ContentTemplate;
        this.SelectedContentTemplateSelector = selectedTabItem.ContentTemplateSelector;
        this.SelectedContentStringFormat = selectedTabItem.ContentStringFormat;
      }
      else
      {
        // Use the content settings from the tab control
        this.SelectedContentTemplate = this.ContentTemplate;
        this.SelectedContentTemplateSelector = this.ContentTemplateSelector;
        this.SelectedContentStringFormat = this.ContentStringFormat;
      }
    }
  }
}

As you can see, the ContentTemplate is set directly, so updating it after this method has completed doesn’t do anything until the method is invoked again. This method is a private method that is called when one of the following events happen:

  • A new template is applied to the TabControl (TabControl.OnApplyTemplate).
  • Another tab page is selected (TabControl.OnSelectionChanged).
  • TabControl’s ItemGenerator status is updated (TabControl.OnGeneratorStatusChanged).

Changes made to the TabControl’s ContentTemplate will therefore only be propagated, when one of these events occur. If you change the ContentTemplate afterwards, then you are out of luck.

How to fix it?
The best way to solve this issue is to use binding, because the binding mechanism will ensure that all properties are updated properly. Unfortunately, you cannot do this, because some of the required properties are read-only and cannot be modified from your code.

The only way to fix it is to override the property metadata for these dependency properties and make sure that the TabControl.UpdateSelectedContent() method is invoked when the dependency property is changed. You cannot call this method directly, but you can call it by calling the public TabControl.OnApplyTemplate() method. The same fix should be made for the TabItem, because if the TabItem has its own template, then this template is used.

I have created a sample application that includes the MyTabControl and MyTabItem classes that contain this workaround.
Tab Control bug example

Publish SABnzbd via Bonjour

If you have AVAHI installed on your FreeBSD box, then create a file called sabnzbd.service in the /usr/local/etc/avahi/services directory. This will publish the server via Bonjour, so you can access it easily using a Bonjour enabled browser, such as Safari.

The sabnzbd.service file should have the following content:

<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">

<!-- See avahi.service(5) for more information about this configuration file -->

<service-group>

  <name replace-wildcards="yes">SABnzbd [%h]</name>

  <service>
    <type>_http._tcp</type>
    <port>8080</port>
    <txt-record>path=/queue</txt-record>
  </service>

</service-group>

Make sure you change the port number if you don’t use the standard SABnzbd port.

Install SABnzbd on FreeBSD

FreeBSD comes with an extensive ports collection that contains SABnzbd. Unfortunately, even in the latest FreeBSD 8.1 release, it contains SABnzbd v0.4.12 which is old and lacks a lot of cool features. There wasn’t a clear guide on how to upgrade SABnzbd to the latest version, so I decided to find out how it can be done.

What has changed?
I started out with the v0.4.12 port, but I have updated the Makefile, distinfo, so it will download the latest version of SABnzbd (currently v0.5.3). Because the new version uses a different set of files, the pkg-plist file has been updated to match the files in v0.5.3.

The v0.4.12 distribution installed some files in the Python directory, which I don’t like. It’s not a part of Python, so it shouldn’t install files in the Python directories. All SABnzbd related files are now stored in its own directory. On most installations this will be /usr/local/shared/sabnzbdplus. Even the SABnzbd.py file that is used to run the application is now stored inside this directory. A symbolic link will be created during installation from /usr/local/bin to make sure the program can be found.

Two patches are necessary to the SABnzbd.py file. First Python must be told to first look for packages in the SABnzbd directories, because SABnzbd comes with its own version of CherryPy. Another change is that the SABnzbd directory will be hardcoded to the correct path. The original version tries to detect the path automatically, but this fails when you start it from /usr/local/bin.

The last addition that I made was to include an RC-script that allows that SABnzbd can be started as a daemon during system startup.

How to install SABnzbd
The following steps should be performed to download and install SABnzbd on your FreeBSD installation:

  1. Log on with root privileges on your FreeBSD server.
  2. Go to the ports directory.
  3. Fetch the latest version of my distribution.
  4. Unpack the archive and remove the archive itself.
  5. Change to the new SABnzbd directory.
  6. Install the new version and clean up.

In code this looks like:

[root@freebsd /]# cd /usr/ports/news
[root@freebsd /usr/ports/news]# wget http://blog.ramondeklein.nl/wp-content/uploads/2010/07/sabnzbdplus-0.5.3.tar.gz
[root@freebsd /usr/ports/news]# tar -xvzf sabnzbdplus-0.5.3.tar.gz
[root@freebsd /usr/ports/news]# rm sabnzbdplus-0.5.3.tar.gz
[root@freebsd /usr/ports/news]# cd sabnzbdplus-0.5.3
[root@freebsd /usr/ports/news/sabnzbdplus-0.5.3]# make install && make clean

Now you can start SABnzbd by typing SABnzbd.py. Go to http://your-freebsd-server:8080 to check if the server is running correctly. Refer to the SABnzbd manuals for more information about configuring SABnzbd itself.

Read the next paragraphs if you want to run SABnzbd as a daemon with more restrictive privileges.

How to run SABnzbd as a daemon
Add the following lines to your /etc/rc.conf file to make sure it starts during system startup:

# SABnzbd settings
sabnzbd_enable="YES"

The default daemon runs as root, but it is better to run SABnzbd under a less-privileged account. Add the following lines to your /etc/rc.conf file to make sure the daemon runs under the ‘ramon’ account:

# SABnzbd settings
sabnzbd_enable="YES"
sabnzbd_user="ramon"

You don’t need to restart the system to test if the daemon can be started. Type the following command to start the SABnzbd daemon:

/usr/local/etc/rc.d/sabnzbd start

Stopping the daemon can be done using the following command:

/usr/local/etc/rc.d/sabnzbd stop

Always make sure you stop the daemon before you make any changes to the configuration files by hand.

Known issues

  • Make sure you run SABnzbd.py with root privileges at least once. This enables that the compiled versions can be stored. Other users probably don’t have the required privileges to store these files.
  • During deinstallation the compiled Python files are not removed. This result in some error messages. You can delete these files yourself when you deinstall the distribution.

SABnzbd distributions
The following list contains the SABnzbd distributions that I have created:

Beware of routed events

WPF introduces the concept of routed events. There are different routing strategies, but the most commonly used strategy is the bubbling strategy. With the bubbling strategy, the event handlers on the event source are invoked. The routed event then routes to successive parent elements until reaching the element tree root or when a handler sets the Handled property.

Always make sure that you set the Handled property, when you handle a routed event. It’s easy to forget, but the framework walks up the tree to find another handler. This has a performance penalty, but it might have unwanted side-effects that are even worse.

When you use controls that can be nested, then you should take extra care when handling routed events. Suppose we have the following code:

<Expander x:Name="Expander1" Header="Expander 1" Collapsed="OnExpander1Collapsed">
  <Expander x:Name="Expander2" Header="Expander 2">
    <TextBlock Text="Routed event test application."/>
  </Expander>
</Expander>

And we implement the Collapsed handler like this:

private void OnExpander1Collapsed(object sender, RoutedEventArgs e)
{
   MessageBox.Show("Expander 1 is collapsed.");
   e.Handled = true;
}

When you collapse Expander 1, then the message is shown. But guess what happens if you collapse the inner expander? The inner Expander raises the Collapsed event and the framework will search for an appropriate handler. The Collapsed event is defined as a bubbling event, so it will start at the source (the inner expander) and will go up the visual tree. When it reaches the outer expander, then it will call the event handler.

You can solve the problem by adding an event handler for the inner expander and set its Handled property. It works in this example, but it won’t if a third-party control raised the event. The only way to solve this problem is to check the source of the event, so the event handler should read:

private void OnExpander1Collapsed(object sender, RoutedEventArgs e)
{
  if (e.OriginalSource == this.Expander1)
  {
    MessageBox.Show("Expander 1 is collapsed.");
    e.Handled = true;
  }
}

In this example the problem is clearly visible. But things can get really nasty in more complex scenarios. Suppose you use an expander control in your code and some changes the template of another control so it uses an expander too. If this control is used within your expander, then things might get pretty nasty if you handle the Expanded and/or Collapsed events.

So make sure you check the source of the event for routed events. The disadvantage is that you have to name your control, which adds additional fields to your class. If the expander is inside a data template, then checking the name is a little bit more difficult, but not impossible:

private void OnExpander1Collapsed(object sender, RoutedEventArgs e)
{
  var originalSource = e.OriginalSource as Framework;
  if ((originalSource != null) && (originalSource.Name == "Expander1"))
  {
    MessageBox.Show("Expander 1 is collapsed.");
    e.Handled = true;
  }
}

Windows Phone 7 SDK

This Windows Phone 7 SDK has been released this week and I must say that I am impressed and disappointed at the same time. Let’s start with the positive things:

  • WP7 applications use Silverlight 4, which is a great and elegant programming model.
  • A WP7 simulator is included which works fast and easy.
  • The SDK fully integrates in VS2010.NET

The negatives:

  • It uses a subset of Silverlight 4, which lacks a lot of functionality (at this moment?). Even a simple DockPanel is not available (although this is easy to implement).
  • No local database support. There are some open-source solutions, but I would rather use something like SQL-Server for Mobile.

The programming model is much better than Apple’s Cocoa Touch framework. Silverlight is much more elegant compared to the ancient Objective-C stuff that Cocoa Touch uses. The drawback is that Apple’s system is more mature. You can access a standard database and it ships which better controls than WP7 does. Maybe things get better before WP7 ships, but there is still a lot of work to do for Microsoft.