Why is my zoomable ScrollViewer snapping the image to the left?

Why is my zoomable ScrollViewer snapping the image to the left?

FlipView is an interesting control that can be easily used for scenarios such as image galleries. Other than flipping through a bunch of images, users normally expect to be able to zoom in and out, and we all now that zooming in and out is easiest to implement with a ScrollViewer with enabled ZoomMode property. However, the default behavior of the ScrollViewer is a bit unusual.

Let's create s simple repro case. Let's define a very simple model class:

public class WindowsWallpaper  
{
    public string ImageUri { get; set; }
}

and a very simple HomePageViewModel class:

public class HomePageViewModel : INotifyPropertyChanged  
{
    public HomePageViewModel()
    {
        this.Wallpapers = new List<WindowsWallpaper>
        {
            new WindowsWallpaper
            {
                ImageUri = "img13.jpg"
            }
        };
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public List<WindowsWallpaper> Wallpapers { get; private set; }

    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        if (this.PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

The MainPageViewModel will hold a list of wallpapers, although we'll only use one for this demo, and it will be the popular Windows 10 hero image (that I previously added to the Visual Studio project).

public sealed partial class MainPage : Page  
{
    public HomePageViewModel ViewModel { get; set; }

    public MainPage()
    {
        this.InitializeComponent();

        this.ViewModel = new HomePageViewModel();
        this.DataContext = ViewModel;
    }
}

XAML will be a somewhat naïve implementation of a FlipView picture gallery:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">  
    <FlipView ItemsSource="{Binding Wallpapers}">
        <FlipView.ItemTemplate>
            <DataTemplate>
                <ScrollViewer ZoomMode="Enabled" 
                            MinZoomFactor="1" 
                            MaxZoomFactor="4">
                    <Image Source="{Binding ImageUri}" />
                </ScrollViewer>
            </DataTemplate>
        </FlipView.ItemTemplate>
    </FlipView>
</Grid>  

FlipView's ItemsSource is simply the Wallpapers property defined in MainPageViewModel and the DataTemplate consists of a ScrollViewer with enabled ZoomMode and Image as content.

This is the simplest possible implementation, but when you try it, it behaves somewhat unexpectedly.

ScrollViewer moves image to top-left position

I had this problem in Windows 8.1 and it still happens in Windows 10. And I still see StackOverflow questions where people have this issue. Luckily, there's a pretty easy solution.

Taming the ScrollViewer

Taming the ScrollViewer is pretty easy. All you need to do is set the HorizontalScrollBarVisibility and VerticalScrollBarVisibility to Auto. Default value is Disabled, which means that you cannot scroll anywhere, which is why zooming in the image would end up moving you back to the initial position.

<ScrollViewer ZoomMode="Enabled"  
              MinZoomFactor="1" 
              MaxZoomFactor="4"
              HorizontalScrollBarVisibility="Auto"
              VerticalScrollBarVisibility="Auto">

However, just setting those properties to Auto won't be enough, because you would normally use an image larger than window/page (or whatever is hosting the ScrollViewer) to ensure that the image quality is decent even when users zoom in. So, setting the HorizontalScrollBarVisibility and VerticalScrollBarVisibility to Auto for a large image results in the image being already in it's full size at minimum zoom level (ScrollViewer adjusts and gives the images as much space as it needs).

Image already too large at minimum zoom level

This means that we need a way to constraint the size of the image. The easiest way to accomplish that is to set the MaxWidth and MaxHeight values to the size of the current window/page (or, again, whatever is hosting the ScrollViewer in your case).

Let's add two new properties to the MainPageViewModel. We'll use those properties to set the MaxWidth and MaxHeight of the Image object:

private double pageWidth;  
private double pageHeight;

public double PageWidth  
{
    get
    {
        return this.pageWidth;
    }
    set
    {
        if (this.pageWidth != value)
        {
            this.pageWidth = value;
            NotifyPropertyChanged();
        }
    }
}

public double PageHeight  
{
    get
    {
        return this.pageHeight;
    }
    set
    {
        if (this.pageHeight != value)
        {
            this.pageHeight = value;
            NotifyPropertyChanged();
        }
    }
}

Using the SizeChanged event, we can react to size changes of any FrameworkElement that holds the ScrollViewer (in this case, that' the Page) and set the MaxWidth/MaxHeight of an image to PageWidth/PageHeight of the backing viewmodel. Also, let's set the page name to something simple like MyPage, because we'll use that for ElementName binding.

<Page  
    x:Class="ScrollViewerZoom.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:ScrollViewerZoom"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" 
    x:Name="MyPage" 
    SizeChanged="Page_SizeChanged">
private void Page_SizeChanged(object sender, SizeChangedEventArgs e)  
{
    this.ViewModel.PageWidth = e.NewSize.Width;
    this.ViewModel.PageHeight = e.NewSize.Height;
}

In the end, all you have to do is bind the MaxWidth and MaxHeight Image properties to PageWidth and PageHeight of the backing viewmodel, and that's it!

<ScrollViewer ZoomMode="Enabled"  
              MinZoomFactor="1" 
              MaxZoomFactor="4"
              HorizontalScrollBarVisibility="Visible"
              VerticalScrollBarVisibility="Visible">
    <Image Source="{Binding ImageUri}"
            MaxWidth="{Binding DataContext.PageWidth, ElementName=MyPage}"
            MaxHeight="{Binding DataContext.PageHeight, ElementName=MyPage}"/>
</ScrollViewer>  

Zooming in works as expected!

Image properly zooming in and out

Conclusion

ScrollViewer offers an easy way to zoom in and pan images, but it can be a bit tricky at first. Luckily, once you know what's going on and what you need to do, it's pretty straightforward to tame it, especially in combination with FlipView, to create great image gallery experiences!

Igor Ralic

igor ralic

View Comments
Microsoft Certified Solutions Developer: Windows Store Apps in C#