Nerdy tidbits from my life as a software engineer

Wednesday, December 19, 2007

DataTemplates and UserControls

I ran into a bizarre problem yesterday that racked my brain for a few hours into this morning. What I had was a UserControl with all sorts of custom logic; it's own DependencyProperties, RoutedEvents, and complicated bindings. All of this logic required, as you would expect, to have explicit knowledge of the names of various components inside it (so that a particular ComboBox's SelectedIndex could be set in procedural code when something happened).

So my UserControl works great; it's nice and spiffy and does all sorts of fun things. Then I wanted to take the UserControl and put it into a TabControl, so that each TabItem would have its own instance of my UserControl inside of it. It looked something like this:

<DataTemplate x:Key="mTabItemDataTemplate">
<ctrls:TabItemControl DataContext="{Binding}" />
</DataTemplate>
...
<TabControl x:Name="mTabControl"
Grid.Column="0"
Grid.Row="0"
SelectionChanged="OnTabControlSelectionChanged"
ItemsSource="{Binding TabItems,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}}}"

ContentTemplate="{StaticResource mTabItemDataTemplate}" />

Can you guess what happens in this situation? Believe it or not, only one instance of the TabItemControl gets instantiated in this circumstance, no matter how many items are put into the TabItems collection. That means that all the code inside the TabItemControl class that references controls by name is out of whack, because they all reference the same control! So you could be moving a Slider on tab #3, and nothing happens - but on tab #1, something is reacting to the event.

It's a design decision that makes sense, but what I would like to know is whether there is a way to explicitly tell the WPF to create new instances of each DataTemplate for each item that it is bound to (or maybe there's a trick I'm not aware of?) But if you try to recreate this on your own, set a breakpoint on the constructor of your UserControl, and you'll see that it only gets called one time - no matter how many items go into your item list.

Friday, December 7, 2007

Large Images And SnapToDevicePixels

In an application that I'm writing right now, I have a an image control that displays large .tiff images (about 2205 x 768). Obviously, as an image control, it has the ability to move an image around and zoom into it. For whatever reason, however, the WPF doesn't seem to like zooming into large images. I've found sparse documentation on this issue: it appears as though there is a known bug with certain image resolutions (or ratios). Basically, once the scale transform reaches a certain threshold, it hangs. That threshold seems to be around 2.5:

<Image x:Name="mImage"
BorderThickness="0"
Margin="0"
Padding="0"
Canvas.Top="0"
Canvas.Left="0"
Width="512"
Height="512"
Source={Binding ImageSource}">

<Image.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="mImageTranslateTransform"
X="0"
Y="0" />
<ScaleTransform x:Name="mImageScaleTransform"
CenterX="256"
CenterY="256"
ScaleX="{Binding Value, ElementName=mSlider}"
ScaleY="{Binding Value, ElementName=mSlider}" />
</TransformGroup>
</Image.RenderTransform>

</Image>
...
<Slider x:Name="mSlider"
HorizontalAlignment="Center"
Orientation="Vertical"
ToolTip="Zoom Control"
Value="1"
Maximum="16"
Minimum=".0001" />

As you can see from the above sample, the default image size is 512 x 512. Everything works fine with 512 x 512 images. However, when a 2205 x 768 image is loaded, once that slider is moved beyond 2, the UI freezes! Why? I don't know. Maybe the WPF doesn't like scaling such large images? Or maybe the ratio is such that it barfs a bit? Whatever the reason, I had to make a bit of an odd design choice in order to display the image properly:

<ItemsControl x:Name="mImageItemsControl"
BorderThickness="0"
Margin="0"
Padding="0"
Canvas.Top="0"
Canvas.Left="0"
Width="512"
Height="512"
ItemsSource="{Binding ImageSources}"
ScrollViewer.CanContentScroll="False"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Hidden">

<ItemsControl.RenderTransform>
<TransformGroup>
<TranslateTransform x:Name="mImageTranslateTransform"
X="0"
Y="0" />
<ScaleTransform x:Name="mImageScaleTransform"
CenterX="256"
CenterY="256"
ScaleX="{Binding Value, ElementName=mSlider}"
ScaleY="{Binding Value, ElementName=mSlider}" />
</TransformGroup>
</ItemsControl.RenderTransform>

<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Margin="0"
SnapsToDevicePixels="True"
IsItemsHost="True"
Orientation="Vertical"
IsHitTestVisible="False" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

<ItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"
Margin="0" />
</DataTemplate>
</ItemsControl.ItemTemplate>

</ItemsControl>

That's right: I replaces a single image with a list of images. Internally, instead of reading one image off the disk and creating an image source, I split the image into n images and shove them into a list, which are then displayed in the items control. Is that ridiculous or what? So instead of displaying one large 2205 x 768 image, I display two 768 x 768 images and one 768 x 669 image, on top of each other. Messy, but it works.

One more thing. Notice the StackPanel in the ItemsControl.ItemsPanel? Originally, I was not setting the SnapsToDevicePixels property. The result was a noticeable line between images, which looked as though there was a margin or padding property set somewhere that was causing the stacked images not to display on top of each other. After an hour or so of fooling around, I eventually tried setting the SnapsToDevicePixels property and bam! Everything worked properly. So, if you ever want your ItemsControl items to stack up right next to each other, that's the property that needs to be set so that it happens.