
Sliding Controls Collection
Introduction
This article is the second part of the SlidingControls
series and describes the SlidingPanoramaControl
, a custom WPF control developed to view and rotate 360° panoramic images all around.
Panoramic photography is a technique of photography, using specialized equipment or software, that captures images with elongated fields of view. This field can amount up to 360 ° and, hence, illustrates the surroundings around the camera location, up to the whole all-round view. Such produced pictures normally have a small height and very big width. The following example shows an image with the original size 8.504x673 pixels, scaled proportional to 600x47 pixels conform to the prescribed CodeProject image width:
Jena Panorama image scaled to 600pt width:

There are countless examples of panoramic photographs to find on the Internet using Google Image Search. Please with the use respect particular copyright protection hints. Here is a YouTube video that explain how to take and edit panorama photographs: 360 degree Panorama Photography tips and editing. You can see that such photos are concerning its aspect ratio not suitable to be viewed in its original form. Therefore is an appropriate viewer necessary and this is also the reason why this control was developed.
The Solution
Not to believe but, with the SlidingControls
you have already a solution in your hands. The simplest panoramic viewer is SlidingImageControl
itself. To try this you have to create a WPF application names MyFirstPanoramicViewer
and to add reference to SlidingControls
to extend XAML namespace with:
xmlns:TB="clr-namespace:TB.Instruments;assembly=SlidingControls"
After that you have to:
- Create an instance of
SlidingImageControl
named panorama
;
- Assign a link e.g. [http://upload.wikimedia.org/wikipedia/commons/8/84/Jena_Panorama.jpg] to the
ImageSource
;
- Set both
ImageHorizontalAlignment
and ImageVerticalAlignment
to Stretch
;
- Set
SlidingDirection
to Horizontal
.
And that's all! Congrats, you already have made your first great panoramic image viewer.
All together look like that:
<Window x:Class="MyFirstPanoramicViewer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:TB="clr-namespace:TB.Instruments;assembly=SlidingControls"
Height="400" Width="600" Background="Gray"
Title="MyFirstPanoramicViewer">
<TB:SlidingImageControl Name="panorama"
ImageSource="http://upload.wikimedia.org/wikipedia/commons/8/84/Jena_Panorama.jpg"
ImageHorizontalAlignment = "Stretch"
ImageVerticalAlignment = "Stretch"
SlidingDirection = "Horizontal"/>
</Window>
Panorama 360° image viewer:

Of course, we won’t be satisfied with it. Therefore we creates a new custom control derived from ContentControl
with a few extras like a better image in space orientation or a navigation (rotation) support.
Exposed members:
The SlidingPanoramaControl
class exposes the following members.
Constructors |
Description |
SlidingPanoramaControl() |
Initializes a new instance of the SlidingPanoramaControl class and sets defaults. |
Properties |
Description |
CurrentUnit |
Gets or sets an unit from SlidingUnitType enumeration that specifies the current unit for all unit depend properties: NorthDirection , MouseWhellDelta and Value . |
IsNormalized |
Gets or sets a flag that determine should a value be normalised, which was changed from embedded SlidingImageControl by mouse moving or wheel rotating. |
MouseWheelDelta |
Gets a value that indicates the amount that the panoramic image has changed with every MouseWheel event. Default value is 15 degrees. |
NorthDirection |
Gets or sets the north direction of a panoramic image. Default value is 0 and represent the center of the picture. |
PanoramaSource |
Gets or sets a source of the 360° panoramic image to be shown. |
SlidingUnits |
Gets a description of all defined SlidingUnits . This is useful outside of the class for different converters. |
Value |
Gets or sets an offset to a start image position. The start image position is the NorthDirection deviation from the center. |
Fields |
Description |
PanoramaSourceProperty |
Identifies the PanoramaSourceProperty dependency property. |
ValueProperty |
Identifies the VakueProperty dependency property. |
In different MSDN and StackOverflow discussions can often be read that bitmap images for ImageSource
should be loaded asynchronous before assigning, to avoid to block UI, particularly if you refer to 'slow' sources, big images or bad URLs for example. This is definitely not at all necessary because ImageSource
assignment works already asynchronous. Here is an elegant solution introduced how can you assign ImageSource
from different sources using IsDownloaded
flag without a lot of effort.
The PanoramaSource property defined in SlidingPanoramaControl.cs:
#region PanoramaSource DependencyProperty
public ImageSource PanoramaSource
{
get { return (ImageSource)this.GetValue(PanoramaSourceProperty); }
set { this.SetValue(PanoramaSourceProperty, (object)value); }
}
public static readonly DependencyProperty PanoramaSourceProperty =
SlidingImageControl.ImageSourceProperty.AddOwner(typeof(SlidingPanoramaControl),
new FrameworkPropertyMetadata((ImageSource)null,
new PropertyChangedCallback(OnPanoramaSourcePropertyChanged)));
public static void OnPanoramaSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SlidingPanoramaControl aSlidingPanorama = (SlidingPanoramaControl)d;
BitmapSource aBitmapSource = (ImageSource)e.NewValue as BitmapSource;
if (aBitmapSource != null)
{
if (aBitmapSource.IsDownloading)
{
if (aSlidingPanorama._LoadingIndicator != null)
aSlidingPanorama._LoadingIndicator.Visibility = Visibility.Visible;
return;
}
}
if (aSlidingPanorama._LoadingIndicator != null)
aSlidingPanorama._LoadingIndicator.Visibility = Visibility.Hidden;
aSlidingPanorama.OnPanoramaSourceChanged();
}
private void OnPanoramaSourceChanged()
{
if (_SlidingPanorama != null)
{
_SlidingPanorama.ImageSource = PanoramaSource;
if (PanoramaSource == null)
{
SlidingUnits.SetPixelBounds(0, SlidingUnits.PixelMax);
return;
}
SlidingUnits.SetPixelBounds(0, _SlidingPanorama.ImageSize.Width);
OnMouseWheelDeltaChanged();
OnNorthDirectionChanged();
}
}
#endregion
Here is the CircularProgressBar
from [3] used as a loading progress indicator.
The PanoramaSource property assigned in MainWindow.xaml.cs:
private void sources_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
PanoramaSourceItem source = (PanoramaSourceItem)sources.SelectedItem;
panorama.PanoramaSource = null;
BitmapImage image = new BitmapImage();
try
{
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.UriSource = new Uri(source.UriString, UriKind.RelativeOrAbsolute);
image.EndInit();
}
catch
{
image = null;
}
panorama.PanoramaSource = image;
panorama.NorthDirection = source.NordDirection;
}
The SlidingImageControl
accept only pixels as image position units for the ImageOffset
property, but we maybe want to set MouseWheelDelta
and especially NorthDirection
as an angle unit (deg/rad) and want to set/get the image rotation Value
as an azimuth or number of cycles. To allow this we must define suitable units and must install a ValueConverter
between the ImageOffset
property of the embedded private SlidingImageControl
and the exposed public Value
property of the SlidingPanaoramaControl
itself.
For the conversion we need a lot of parameters, so the first idea was to write a MultiValueConverter
. But MultiValueConverters
are a kind of ‘many-to-one’ converters and will be used mostly only in one direction. In our case the conversion is a typical ‘one-to-one’ - but we need both direction because SlidingImageControl
can change the ImageOffset
too.
Because the ValueConverter
needed several properties from the SlidingPanoramaControl
class as parameters and will be used only in the class, the first decision was to apply an unusual approach and implement the converter directly in the class.
The SlidingPanoramaControl as a ValueConverter for itself:
public class SlidingPanoramaControl : ContentControl, IValueConverter
{
private void SlidingPanoramaControl_OnLoaded(object sender, RoutedEventArgs e)
{
Binding bind = new Binding("Value");
bind.Source = this; bind.Converter = this; bind.Mode = BindingMode.TwoWay; _SlidingPanorama.SetBinding(SlidingImageControl.ImageOffsetProperty, bind);
}
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
}
}
In this manner we come in the situation that these properties can be used directly and they must not be laboriously passed/transported as parameters to the converter. Finally, for this approach we must only in the SlidingPanoramaControl
integrate an IValueConverter
interface and implement two their methods: Convert
and ConvertBack
. Thus the value converter is embedded in the source and both are the same for the binding.
A ValueConverter embedded into the Source:

This solution works well, however, is quite uncommon and not reusable. To avoid this, I have decided to implement a classical separate ValueConverter
derived from MarkupExtension
. Deriving ValueConverter
from MarkupExtension
enables us to use the value converter without making it a static resource. The only problem to be solved was how to get a reference of the source object in the converter. There are three possibilities: through constructor, property or parameter. The third possibility was chosen. So a reference to the source object became a ValueConverter
as a ConverterParameter
using the x:Reference
markup extension, because we are unable to bind directly to ConverterParameter
, because this is not a DependencyProperty
, because Binding
is not a DependencyObject
.
The x:Reference
markup extension is often mistakenly associated with the XAML 2009 features that can only be used from loose XAML at the time of this writing. Although x:Reference is a new feature in WPF 4, it can be used from XAML 2006 just fine as long as your project is targeting version 4 or later of the .NET Framework.
A ValueConverter with a reference to the Source as a ConverterParameter:

There came again an unexpected problem. When using {x:Reference}
, the Visual Studio designer throws an InvalidOperationException
exception with the message "Service provider is missing the INameResolver service."
This is a known issue No.660141 and should be resolved sometime in the future. There are on the internet attempts to handle this problem, however, no one works so properly. Here is introduced an easy and working solution which overrides x:Reference
markup extension with our one called TB:Reference
:
A custom Reference markup extension in SlidingUnitsExtension.cs:
[ContentProperty("Name")]
public class Reference : System.Windows.Markup.Reference
{
public Reference() : base() { }
public Reference(string name) : base(name) { }
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
return null;
return base.ProvideValue(serviceProvider);
}
}
And last but not least, in the same ValueConverter
is included a MultipleValueConverter
used by XAML in a TextBlock
to convert panorama angle unit into another desired unit and to show the result combined with a StringFormat
. Here is the whole masterpiece:
The SlidingUnitConverterExtension super-converter:
#region SlidingUnits converters
[ValueConversion(typeof(double), typeof(double))]
public class SlidingUnitsConverterExtension : MarkupExtension, IValueConverter, IMultiValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
private SlidingPanoramaControl _SPC;
#region SlidingUnits conversion helper methods
#endregion
#region IValueConverter Members
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture) {
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture) {
}
#endregion
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
}
public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
{
}
#endregion
}
#endregion
A usage of the MultiValueConverters part:
<TextBlock
FontFamily="Segoe UI Mono"
FontWeight="Bold"
FontSize="11" >
<TextBlock.Text>
<MultiBinding StringFormat=" Value: {0}"
Converter="{TB:SlidingUnitsConverter}"
ConverterParameter="{TB:Reference panorama}">
<Binding ElementName="panorama" Path="Value"/>
<Binding ElementName="panorama" Path="CurrentUnit"/>
<Binding ElementName="main" Path="DisplayUnit"/>
</MultiBinding>
</TextBlock.Text>
For changing image azimuth is in the early stages used a standard slider control. However, it was fast clear that the slider is not well suitable for this purpose. Late was introduced a new custom control named AngleSelector
. The AngleSelector
can endlessly change an angle, start at the X-Axe and increases clockwise.
But an azimuth should start at Y-Axe and increases clockwise too. For the adaptation was developed a new converter named LinearConverter
as a markup extension with two parameters: m
= slope and b
= Y-Intercept. The LinearConverter
convert a variable X
in another Y
using well known linear equation Y = m * X + b
in both directions.
Images from Wikipedia used for SlidingPanoramicControl.Test solution:
- Jena Panorama: Homepage, Download, Author: Michael Schreiter alias ArtMechanic.
Full resolution: 8.504 x 673 pixels, File size: 5.67 MB, MIME type: image/jpeg, Camera location: 51° 30' 29.39" N, 0° 7' 41.31" W, License: CC-BY-SA 3.0.
- Trafalgar Square: Homepage, Download,Author: David Iliff alias Diliff
Full resolution: 9.932 × 2.075 pixels, File size: 5.67 MB, MIME type: image/jpeg, Camera location: 51° 30' 29.39" N, 0° 7' 41.31" W, License: CC-BY-SA 3.0.
- Lac de Joux: Homepage, Download,Author: Lausanne De Suisse alias 100zax
Full resolution: 6.000 × 697 pixels, File size: 759 KB, MIME type: image/jpeg, Camera location: ??? N, ??? W, License: CC-BY-SA 3.0.
References and third-party software components:
- Dmitri Nesteruk: Unit Conversions in C#/WPF
- VCSKicks: Photoshop-Style Angle and Altitude Selectors
- Sacha Barber: Better WPF Circular Progress Bar
- MSDN Forum: Passing parameter to IValueConverter
- Muhammad Shujaat Siddiqi: WPF - Binding Converter Parameter
- Zeeshan Amjad: Range Converter Revisited
- Walt Ritscher: Simplify your Binding Converter with a Custom Markup Extension
Conclusion
I hope that you picked up something useful from this article. Suggestions and comments are welcome. If you like it, vote please. If you use it, please describe your successful story in the comments below. If you wish to sell it or to distribute it commercially, please contact me.
History
- v1.0 – 5th June, 2012 – The initial release.