WPF ListBox virtualization screws up displayed items - wpf

Problem
We need to efficiently display a large (>1000) number of objects in a WPF ListBox control.
We are relying on the WPF ListBox’s virtualization (via VirtualizingStackPanel) to efficiently display these items.
Bug: The WPF ListBox control does not display items correctly when using virtualization.
How to Reproduce
We have distilled the problem to the standalone xaml shown below.
Copy and paste the xaml into XAMLPad.
Initially, there is no selected item in the ListBox, so as expected, all items are the same size and they completely fill the available space.
Now, click on the first item.
As expected, because of our DataTemplate, the selected item will expand to show additional information.
As expected, this causes the horizontal scrollbar to appear, since the selected item is now wider than the available space.
Now use the mouse to click and drag the horizontal scrollbar to the right.
Bug: the non-selected visible items no longer stretch to fill the available space. All the visible items should be the same width.
Is this a known bug?
Is there any way to fix this, either via XAML or programmatically?
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
<Page.Resources>
<DataTemplate x:Key="MyGroupItemTemplate">
<Border Background="White"
TextElement.Foreground="Black"
BorderThickness="1"
BorderBrush="Black"
CornerRadius="10,10,10,10"
Cursor="Hand"
Padding="5,5,5,5"
Margin="2"
>
<StackPanel>
<TextBlock Text="{Binding Path=Text, FallbackValue=[Content]}" />
<TextBlock x:Name="_details" Visibility="Collapsed" Margin="0,10,0,10" Text="[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx]" />
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type ListBoxItem}},Path=IsSelected}"
Value="True">
<Setter Property="TextElement.FontWeight"
TargetName="_details"
Value="Bold"/>
<Setter Property="Visibility"
TargetName="_details"
Value="Visible"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Page.Resources>
<DockPanel x:Name="LayoutRoot">
<Slider x:Name="_slider"
DockPanel.Dock="Bottom"
Value="{Binding FontSize, ElementName=_list, Mode=TwoWay}"
Maximum="100"
ToolTip="Font Size"
AutoToolTipPlacement="BottomRight"/>
<!--
I want the items in this ListBox to completly fill the available space.
Therefore, I set HorizontalContentAlignment="Stretch".
By default, the WPF ListBox control uses a VirtualizingStackPanel.
This makes it possible to view large numbers of items efficiently.
You can turn on/off this feature by setting the ScrollViewer.CanContentScroll to "True"/"False".
Bug: when virtualization is enabled (ScrollViewer.CanContentScroll="True"), the unselected
ListBox items will no longer stretch to fill the available horizontal space.
The only workaround is to disable virtualization (ScrollViewer.CanContentScroll="False").
-->
<ListBox x:Name="_list"
ScrollViewer.CanContentScroll="True"
Background="Gray"
Foreground="White"
IsSynchronizedWithCurrentItem="True"
TextElement.FontSize="28"
HorizontalContentAlignment="Stretch"
ItemTemplate="{DynamicResource MyGroupItemTemplate}">
<TextBlock Text="[1] This is item 1." />
<TextBlock Text="[2] This is item 2." />
<TextBlock Text="[3] This is item 3." />
<TextBlock Text="[4] This is item 4." />
<TextBlock Text="[5] This is item 5." />
<TextBlock Text="[6] This is item 6." />
<TextBlock Text="[7] This is item 7." />
<TextBlock Text="[8] This is item 8." />
<TextBlock Text="[9] This is item 9." />
<TextBlock Text="[10] This is item 10." />
</ListBox>
</DockPanel>
</Page>

I spent more time attempting this than I probably should have, and couldn't get it to work. I understand what's going on here, but in pure XAML, I'm having trouble figuring out how to solve the issue. I think I see how to solve the problem, but it involves a converter.
Warning: Things are going to get complicated as I explain my conclusions.
The underlying problem comes from the fact that the Width of the controls stretch to the Width of their container. When virtualization is enabled, the Width will not change. In the underlying ScrollViewer inside of ListBox, the ViewportWidth property corresponds to the Width you see. When another control stretches out further (you select it), the ViewportWidth is still the same, but the ExtentWidth shows the full width. Binding the width of all controls to that of the ExtentWidth should work...
But it doesn't. I set the FontSize to 100 for quicker testing in my case. When an item is selected, ExtentWidth="4109.13. Going down the tree to your ControlTemplate's Border, I see ActualWidth="4107.13". Why the 2 pixel difference? ListBoxItem contains a Border with 2 Pixel padding, causing the ContentPresenter to render slightly smaller.
I added the following Style with help from here to allow me to directly access the ExtentWidth:
<Style x:Key="{x:Type ListBox}" TargetType="ListBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border
Name="Border"
Background="White"
BorderBrush="Black"
BorderThickness="1"
CornerRadius="2">
<ScrollViewer
Name="scrollViewer"
Margin="0"
Focusable="false">
<StackPanel IsItemsHost="True" />
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="Border" Property="Background"
Value="White" />
<Setter TargetName="Border" Property="BorderBrush"
Value="Black" />
</Trigger>
<Trigger Property="IsGrouping" Value="true">
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Note I added a name to ScrollViewer for this purpose.
Then, I attempted to bind the Width of your Border to the ExtentWidth:
Width="{Binding ElementName=scrollViewer, Path=ExtentWidth}"
However, because of that 2 pixel padding, the controls will resize in an infinite loop, with the padding adding 2 pixels to the ExtentWidth, which resizes the Border width, which adds 2 more pixels to the ExtentWidth, etc. until you delete the code and refresh.
If you added a Converter that subtracted 2 from the ExtentWidth, I think this might work. However, when the scroll bar does not exist (you have not selected anything), ExtentWidth="0". Thus, binding to MinWidth instead of Width may work better so the items appear correctly when no scroll bar is visible:
MinWidth="{Binding ElementName=scrollViewer, Path=ExtentWidth, Converter={StaticResource PaddingSubtractor}}"
A better solution would be if you could directly databind the MinWidth of the ListBoxItem itself. You could bind directly to ExtentWidth, and no converter would be necessary. However I have no idea how to get access to that item.
Edit: For organization sake, here's the clip required to do that. Makes everything else unnecessary:
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="MinWidth" Value="{Binding Path=ExtentWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}" />
</Style>

Thanks to Will's great analysis!
Based on Will's suggestion: "A better solution would be if you could directly databind the MinWidth of the ListBoxItem itself...However I have no idea how to get access to that item", I was able to implement that using pure xaml, as follows:
<ListBox x:Name="_list"
Background="Gray"
Foreground="White"
IsSynchronizedWithCurrentItem="True"
TextElement.FontSize="28"
HorizontalContentAlignment="Stretch"
ItemTemplate="{DynamicResource MyGroupItemTemplate}">
<!-- Here is Will's suggestion, implemented in pure xaml. Seems to work.
Next problem is if you drag the Slider to the right to increase the FontSize.
This will make the horizontal scroll bar appear, as expected.
Problem: the horizontal scroll bar never goes away if you drag the Slider to the left to reduce the FontSize.
-->
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="MinWidth" Value="{Binding Path=ExtentWidth, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollViewer}}}" />
</Style>
</ListBox.Resources>
<TextBlock Text="[1] This is item 1." />
<TextBlock Text="[2] This is item 2." />
<TextBlock Text="[3] This is item 3." />
<TextBlock Text="[4] This is item 4." />
<TextBlock Text="[5] This is item 5." />
<TextBlock Text="[6] This is item 6." />
<TextBlock Text="[7] This is item 7." />
<TextBlock Text="[8] This is item 8." />
<TextBlock Text="[9] This is item 9." />
<TextBlock Text="[10] This is item 10." />
</ListBox>
I got the idea from Adam Nathan's great book, "Windows Presentation Foundation Unleashed".
So, this seems to fix the original problem.
New Problem
You notice that there is a Slider control in the xaml that let's you increase/decrease the ListBox font. The idea here was to allow the user the ability to scale the ListBox content up or down for easier visibility.
If you first drag the Slider to the right to increase the FontSize, this will make the horizontal scroll bar appear, as expected. The new problem is that the horizontal scroll bar never goes away if you drag the Slider to the left to reduce the FontSize.
Any ideas?

Related

Excess border selection in WPF's Lisbox [duplicate]

I have a ListBox in which each item is a StackPanel. The StackPanel consist of an Image and a TextBlock below it:
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="10">
<Image>
<Image.Source>
<BitmapImage UriSource="{Binding Path=ImageFilePath}"/>
</Image.Source>
</Image>
<TextBlock Text="Title" TextAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
It looks like this:
When the user select an item, I get the default blue rectangle that surround the StackPanel:
Now, I want to make a different border for the selected-item, but I want it to surround only the image.
I know how to make a control template and put a custom border around the ContentPresenter, but this, of course, will surround the whole StackPanel, not only the Image.
I don’t know if making changes to the ContentPresenter is possible, and if it is a good idea at all. If there is other way to achieve the look I want, it will be fine as well.
Right, the ListBox's own ContentPresenter isn't helpful for what you're doing. You want to a) eliminate the ListBox's own selection visuals and b) replace them with something more suitable in the DataTemplate for your items.
The default selection visual is applied by the default template for ListBoxItem. So replace that template. Using a Style in the resources for your ListBox, apply your own control template to ListBoxItem. Not much to it, just present the content and don't provide a selection background. Then you handle the selection visuals with a trigger in your data template, where your image and your label are defined and you can apply changes to one and not the other. The below example works for me.
Note that there's some fiddling with the HorizontalAlignment on the Border element to make it cling to the Image element within it. Also, I wrote a quickie test viewmodel whose Items property is called Items; I assume this is not the name of the collection member you're using to populate your own ListBox.
<ListBox
Margin="8"
ItemsSource="{Binding Items}"
>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Border
x:Name="HighlightBorder"
BorderThickness="4"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10"
>
<Border.Style>
<Style TargetType="Border">
<!-- MUST set default BorderBrush via a style, if you set it at all.
As an attribute on the Border tag, it would override the effects of
the trigger below.
-->
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Border.Style>
<Image Source="{Binding ImageFilePath}" />
</Border>
</Grid>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Value="True">
<Setter TargetName="HighlightBorder" Property="BorderBrush" Value="Orange" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Height of WPF TreeViewItem controltemplate not collapsing

I have an issue that I have been trying to solve for a week and have not only played around extensively trying to figure this out but have done lots of research on StackOverFlow and other websites on how to fix this problem. Just to be clear, I have been learning WPF for about 3 or so months and come from WinForms and am still in the learning phase.
Here is my problem.
I have a TreeViewItems that I am adding to a TreeView control. These TreeView items use a Style that creates a custom look that I am trying to accomplish that is pretty much the look and feel of the entire application. The Style uses an explicit Setter.Value against the Template property to create the custom look of the TreeView item. It has its own custom expander arrow, TextBlock header that is bound to the TreeViewItem header, and also of course a ContentPresenter and a ItemsPresenter. There is also a trigger that is wired up to the value of the TreeViewItem's IsExpanded value so that way the ItemsPresenter can be shown or hidden when the TreeViewItem is expanded or collapsed. Everything works as it should except the collapse and expand part. Of course the ItemsPresenter hides and shows like it should but the TreeViewItem itself does not actually collapse its height when the IsExpanded is false. To show what I mean, here are 2 pictures to illustrate what is going on. I added a green border around the grid in the template of the style to show that the individual TreeViewItem itself is not shrinking its "height" when collapsed.
Expanded
Pic of expanded tree view item
Collapsed
Pic of collapsed tree view item
As you can see, the green border, or the treeview item itself is still the same height when collapsed as it is when expanded. Here is the XAML used to create the custom style of the TreeViewItem's themselves.
TreeViewItem Style XAML Code:
<Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeViewItem">
<Border x:Name="MyBorder" BorderThickness="1" BorderBrush="LawnGreen">
<Grid HorizontalAlignment="Left" ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition Height="25"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ui:TreeViewItemExpander x:Name="TreeViewItemExpander" Grid.Row="0" Grid.Column="0" IsPointingDown="{TemplateBinding IsExpanded}"/>
<!--This represents the text for the tree view item itself-->
<TextBlock Text="{TemplateBinding Header}" Grid.Row ="0" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Center" Foreground="White"/>
<ContentPresenter x:Name="ContentPresenter" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<ItemsPresenter x:Name="ItemsPresenter" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Hidden"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="ItemsPresenter" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This code below is how I am using the style
<TreeView Style="{StaticResource TreeViewStyle}" Width="200" Margin="419,337,19,328">
<controls:CustomTreeViewItem Header="Folder 1" Style="{StaticResource TreeViewItemStyle}">
<Button Content="Item 1" HorizontalAlignment="Left" VerticalAlignment="Top" Height="20"/>
<Button Content="Item 2" HorizontalAlignment="Left" VerticalAlignment="Top" Height="20"/>
</controls:CustomTreeViewItem>
</TreeView>
Thanks for any input or help that anybody can provide. I hope that I was clear enough.
I figured it out. What I did was just add a setter in the IsExpanded trigger to set the property of the grid row that the ItemsPresenter resides in. All I did was set the height of the row to 0 which essentially hides the items. Here is the code of the trigger itself from the code above with the change applied.
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter TargetName="Items" Property="Visibility" Value="Visible"/>
<Setter TargetName="ItemsRow" Property="Height" Value="0"/>
</Trigger>
</ControlTemplate.Triggers>
I am interested though if anybody else has a better solution. After I figured this out, I then thought about animating the height of the row until it is 0 to give a better effect, but was not successful. I found out that StoryBoards are freezable and are frozen when inside of a Style or ControlTemplate. That means if you wanted it to be animated, then the animation of the item collapsing would have to be implemented with code behind, or on each TreeViewItem individually? I am sure there is a better way. If I find out how to accomplish this, I will update this post for everybody to reference. Please feel free to add to this post on a better solution!!

sort of special accordion like control

I have put an example link in here:
http://activeden.net/item/xml-horizontal-vertical-accordion-banner-rotator/full_screen_preview/127714?ref=premiumtemplates
I try to achieve something similar (but far more basic) with WPF.
Not the flying text stuff, only the basic navigation idea.
I tried to build it with some expander controls and a stackpanel.
What I came up with is this:
<ItemsControl Grid.Row="1" IsTabStop="False" ItemsSource="{Binding Path=tabs,Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Orientation="Vertical" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
I use MVVM, so there is also a template which is applied:
<DataTemplate DataType="{x:Type vm:TabulatorViewModel}">
<Expander HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ExpandDirection="{Binding .direction,Mode=OneWay}" IsExpanded="{Binding .isExpanded,Mode=TwoWay}" Header="{Binding .header,Mode=OneWay}" >
<Expander.Style>
<Style TargetType="{x:Type Control}">
<Setter Property="Template" Value="{StaticResource HorizontalExpanderRight}" />
<Style.Triggers>
<DataTrigger Binding="{Binding .direction}" Value="Left">
<Setter Property="Template" Value="{StaticResource HorizontalExpanderLeft}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Expander.Style>
<StackPanel>
<Label Content="{Binding .seitenInhalt.Header,Mode=OneWay}"></Label>
<TextBox Text="{Binding .seitenInhalt.Inhalt,Mode=OneWay}"></TextBox>
<Button Content="zurück" Command="{Binding .seitenInhalt.MovePreviousCommand}" />
<Button Content="vor" Command="{Binding .seitenInhalt.MoveNextCommand}"/>
</StackPanel>
</Expander>
</DataTemplate>
So, this is working, at least kind of.
two screenshots from my current project to explain the issues:
Could not post picture because of reputation points.
All Items together should use the complete width of the stackpanel, not like in the picture. Could not post picture because of reputation points.
All items should use the complete width, but the one expanded item should have a bigger width then the rest. As on the picture, but the collapsed items should use the remaining space, each by the same amount filling the gap)
Any help would be great, I hope it is possible to understand my goal / issues.
I think what you want to use is a UniformGridPanel or WrapPanel, which will use the full width available, rather than a StackPanel which is made to use the minimum width possible. The panel overview is here.
You'll probably want to use a Grid with the Height/Width of the Columns/Rows set to * if the item is Expanded, or Auto if not.
Also, if you're using a Grid in an ItemsControl, you need to set the Grid.Row/Grid.Column, and Width/Height properties on the ContentPresenter, not the ItemTemplate since the ItemTemplate in a ItemsControl is always wrapped in a ContentPresenter.

ZOrder of Expander with Canvas inside of List

I have a list which contains some controls, including an Expander. Within the expander is another list, which I want to overlay the outer list. Here's a simple repro:
<Page.Resources>
<x:Array x:Key="array1" Type="sys:String">
<sys:String>item 1</sys:String>
<sys:String>item 2</sys:String>
<sys:String>item 3</sys:String>
</x:Array>
<DataTemplate x:Key="buttonTemplate">
<Button Content="{Binding}"/>
</DataTemplate>
<DataTemplate x:Key="expanderItem">
<StackPanel>
<Expander Header="Options">
<Canvas>
<StackPanel Panel.ZIndex="999" Background="Red">
<Label>A1</Label>
<Label>A2</Label>
<Label>A3</Label>
<Label>A4</Label>
</StackPanel>
</Canvas>
</Expander>
<Label BorderBrush="Black" BorderThickness="2" Content="{Binding}"/>
</StackPanel>
</DataTemplate>
</Page.Resources>
<Grid>
<ListBox ItemsSource="{StaticResource array1}" ItemTemplate="{StaticResource expanderItem}"/>
</Grid>
When the Expander gets opened, the inner labels get rendered at the same level as the label in the same DataTemplate and the contents later items in the list. I have tried moving the Panel.ZIndex up to the panel with no change.
If I add the following style:
<Style TargetType="{x:Type Expander}">
<Style.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Setter Property="Panel.ZIndex" Value="999"/>
</Trigger>
</Style.Triggers>
</Style>
It will properly overlay items in the SAME list item, but still renders intermixed with contents from later list items.
(I suspect this is a fairly obvious layout problem, but I have not been able to find it.)
You could try writing a converter that sets the ZIndex in the datatemplate based on the index of the item in the list. The interesting thing with this would be making sure everything updates correctly as items are added / removed.
Do you need the expander to be independent of layout?

WPF Image Zooming

I have a Viewbox with an Image inside of it. This is great since the Viewbox will scale the Image to fit the window. However, I need to be able to zoom the image to its full size and show scroll bars and I am having a hard time figuring out how to do this.
Here's what I have right now. Can anyone give some pointers on how I can modify this to implement the above functionality?
<Viewbox x:Name="viewbox">
<StackPanel>
<Image x:Name="image" Source="ranch.jpg" />
</StackPanel>
</Viewbox>
Edit:
Just to clarify. I need both ways of viewing the image, the viewbox style of fitting the window AND the ability to toggle to an Actual Size view that shows scrollbars and doesn't resize the image.
You don't need a Viewbox here, by putting the Image in a ScrollViewer and manipulating the VerticalScrollBarVisibility and HorizontalScrollBarVisibility properties, you can make the Image scale or not:
<Grid xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<CheckBox x:Name="chkActualSize" Grid.Row="0" Content="Actual Size"/>
<ScrollViewer Grid.Row="1">
<ScrollViewer.Style>
<Style TargetType="{x:Type ScrollViewer}">
<Setter Property="HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="VerticalScrollBarVisibility" Value="Disabled"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=chkActualSize}" Value="True">
<Setter Property="HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ScrollViewer.Style>
<Image Source="http://sipi.usc.edu/database/misc/4.1.01.tiff" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</ScrollViewer>
</Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<Viewbox>
<Image Source="ranch.jpg"/>
</Viewbox>
</ScrollViewer>
Based on your edit that you need to toggle the two approaches, I would do this in one of two ways.
Have two elements with the image. The Image element inside a ScrollViewer without the Viewbox will give you the full size image, and the Viewbox version will scale it. Then you can toggle the two depending on what you want to show.
Use a binding expression on the Height and Width properties of the Image and enclose it inside the scrollviewer. When you want to scale it (in some sort of trigger), set the Height to a binding expression that accesses the ActualHeight property of the ScrollViewer or whatever container is just above that (using RelativeSource to access the nearest ancestor something like the following):
{Binding Path=ActualHeight,
RelativeSource={RelativeSource AncestorType={x:Type ScrollViewer}}}
Thought I would post my solution for anyone looking.
<Slider Width="200" Value="500" Interval="25" Maximum="1000" x:Name="TestImageSlider" Minimum="-50" />
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto">
<Image Source="{Binding SelectedScannedImage.ScannedImage}" Width="{Binding Path=Value, ElementName=TestImageSlider}" />
</ScrollViewer>

Resources