Monday, December 14, 2009

Five Minute Silverlight 4 Aides-Memoire #7 – IDataErrorInfo

The XAML:

DATA INPUT
  1. <Grid x:Name="LayoutRoot">
  2.   <Grid.Resources>
  3.     <local:Person x:Key="NewJoiner" Age="49" Forename="Fred" Surname="Flintstone" />
  4.   </Grid.Resources>
  5.   <Grid DataContext="{StaticResource NewJoiner}">
  6.     <Grid.ColumnDefinitions>
  7.       <columndefinition Width="1*" />
  8.       <columndefinition Width="5*" />
  9.     </Grid.ColumnDefinitions>
  10.     <Grid.RowDefinitions>
  11.       <rowdefinition Height="50" />
  12.       <rowdefinition Height="50" />
  13.       <rowdefinition Height="50" />
  14.     </Grid.RowDefinitions>
  15.     <Grid.Resources>
  16.       <Style TargetType="TextBlock">
  17.         <setter Property="Margin" Value="10" />
  18.         <setter Property="HorizontalAlignment" Value="Right" />
  19.         <setter Property="VerticalAlignment" Value="Center" />
  20.       </Style>
  21.       <Style TargetType="TextBox">
  22.         <setter Property="Margin" Value="10" />
  23.         <setter Property="Width" Value="154" />
  24.         <setter Property="HorizontalAlignment" Value="Left" />
  25.         <setter Property="VerticalAlignment" Value="Center" />
  26.       </Style>
  27.     </Grid.Resources>
  28.     <TextBlock Text="First Name" Grid.Row="0" Grid.Column="0" />
  29.     <TextBlock Text="Last Name" Grid.Row="1" Grid.Column="0" />
  30.     <TextBlock Text="Age" Grid.Row="2" Grid.Column="0" />
  31.     <textbox x:Name="Forename" Grid.Row="0" Grid.Column="1" Text="{Binding Forename, Mode=TwoWay, ValidatesOnDataErrors=True}" />
  32.     <textbox x:Name="Surname" Grid.Row="1" Grid.Column="1" Text="{Binding Surname, Mode=TwoWay, ValidatesOnDataErrors=True}" />
  33.     <textbox x:Name="Age" Grid.Row="2" Grid.Column="1" Text="{Binding Age, Mode=TwoWay, ValidatesOnDataErrors=True}" />
  34.   </Grid>
  35. </Grid>

The code:

IMPLEMENTATION
  1. public class Person : IDataErrorInfo
  2. {
  3.     public string Forename { get; set; }
  4.     public string Surname { get; set; }
  5.     public int Age { get; set; }
  6.     public string Error { get { return null; } }
  7.  
  8.     public string this[string columnName]
  9.     {
  10.         get
  11.         {
  12.             string error = null;
  13.             switch (columnName)
  14.             {
  15.                 case "Forename":
  16.                     if (string.IsNullOrEmpty(Forename))
  17.                         error = "Forename required";
  18.                     break;
  19.                 case "Surname":
  20.                     if (string.IsNullOrEmpty(this.Surname))
  21.                         error = "Surname required";
  22.                     break;
  23.                 case "Age":
  24.                     if (this.Age < 0 || this.Age > 130)
  25.                         error = "Invalid Age";
  26.                     break;
  27.             }
  28.             return error;
  29.         }
  30.     }
  31. }


The result:

image

Five Minute Silverlight 4 Aides-Memoire #6 – Right Mouse Event Support

<Popup x:Name="PopupMenu" IsOpen="False">
    <ListBox x:Name="MenuItems">
        <ListBoxItem Content="Item 1" />
        <ListBoxItem Content="Item 2" />
        <ListBoxItem Content="Item 3" />
        <ListBoxItem Content="Item 4" />
    </ListBox>
</Popup>


public MainPage()
{
    InitializeComponent();
    MouseRightButtonDown += (s, e) => e.Handled = true;
    MouseRightButtonUp += MainPage_MouseRightButtonUp;
    MenuItems.MouseLeftButtonUp += MenuItems_MouseLeftButtonUp;
}


void MainPage_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    this.PopupMenu.HorizontalOffset = e.GetPosition(this).X;
    this.PopupMenu.VerticalOffset = e.GetPosition(this).Y;
    this.PopupMenu.IsOpen = true;
}

void MenuItems_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    PopupMenu.IsOpen = false;
    System.Diagnostics.Debug.WriteLine(((sender as ListBox).SelectedItem as ListBoxItem).Content);
}

Five Minute Silverlight 4 Aides-Memoire #5 – Rich Text Area Control

<RichTextArea x:Name="Editor">
    <RichTextArea.Blocks>
        <Paragraph>
            <Run Foreground="Blue" Text="This is "/>
            <Run Foreground="Red" FontWeight="Bold" Text="some sample " />
            <Run FontStyle="Italic" Text="text" />
        </Paragraph>
        <Paragraph>
            <Run Text="Here is "/>
            <Run TextDecorations="Underline" Text="some more" />
        </Paragraph>
    </RichTextArea.Blocks>
</RichTextArea>

void UnderlineSelection_Click(object sender, RoutedEventArgs e)
{
    var currentValue = this.Editor.Selection.GetPropertyValue(TextElement.TextDecorationsProperty);
    TextDecorationCollection setValue = TextDecorations.Underline;
    if (currentValue != DependencyProperty.UnsetValue)
        setValue = (TextDecorationCollection)currentValue == setValue ? null : setValue;
    this.Editor.Selection.SetPropertyValue(TextElement.TextDecorationsProperty, setValue);
}

void ItalicSelection_Click(object sender, RoutedEventArgs e)
{
    var currentValue = this.Editor.Selection.GetPropertyValue(TextElement.FontStyleProperty);
    FontStyle setValue = FontStyles.Italic;
    FontStyle defaultValue = FontStyles.Normal;
    if (currentValue != DependencyProperty.UnsetValue)
        setValue = (FontStyle)currentValue == setValue ? defaultValue : setValue;
    this.Editor.Selection.SetPropertyValue(TextElement.FontStyleProperty, setValue);
}

void BoldSelection_Click(object sender, RoutedEventArgs e)
{
    var currentValue = this.Editor.Selection.GetPropertyValue(TextElement.FontWeightProperty);
    FontWeight setValue = FontWeights.Bold;
    FontWeight defaultValue = FontWeights.Normal;
    if (currentValue != DependencyProperty.UnsetValue)
        setValue = (FontWeight)currentValue == setValue ? defaultValue : setValue;
    this.Editor.Selection.SetPropertyValue(TextElement.FontWeightProperty, setValue);
}

image

Friday, December 04, 2009

Five Minute Silverlight 4 Aides-Memoire #4 – Clipboard Access

private void PopButton_Click(object sender, RoutedEventArgs e)
{
    var newPara = new Paragraph();

    newPara.Inlines.Add(new Run { Text = Clipboard.GetText() });

    this.rightRichTextArea.Blocks.Add(newPara);
}

private void PushButton_Click(object sender, RoutedEventArgs e)
{
    Clipboard.SetText(this.leftRichTextArea.Selection.Text);
}

Tuesday, December 01, 2009

Five Minute Silverlight 4 Aides-Memoire #3 – WebCam/Microphone Device Support

<UserControl.Resources>
<
Style x:Key="ListBoxStyle" TargetType="ListBox">
<
Setter Property="ItemTemplate">
<
Setter.Value>
<
DataTemplate>
<
Image Margin="5" Source="{Binding}" Stretch="UniformToFill" Height="80" VerticalAlignment="Center"/>
</
DataTemplate>
</
Setter.Value>
</
Setter>
<
Setter Property="ItemsPanel">
<
Setter.Value>
<
ItemsPanelTemplate>
<
StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</
ItemsPanelTemplate>
</
Setter.Value>
</
Setter>
</
Style>
</
UserControl.Resources>



<StackPanel Grid.Row="2" Grid.Column="0">
<
Button Content="Start WebCam" Click="StartWebCam_Click" Height="30" />
<
Button Content="Snapshot" Click="Snapshot_Click" Height="30" />
</
StackPanel>
<
Rectangle Grid.Row="0" Stretch="Fill" Grid.Column="1" x:Name="CapturedImage" />
<ListBox Grid.Row="1" Margin="10" Grid.RowSpan="2" Grid.Column="1" x:Name="Snapshots" Style="{StaticResource ListBoxStyle}"/>


private void StartWebCam_Click(object sender, RoutedEventArgs e)
{
if (!CaptureDeviceConfiguration.AllowedDeviceAccess & !CaptureDeviceConfiguration.RequestDeviceAccess())
return;

VideoBrush videoBrush = new VideoBrush();
videoBrush.SetSource(captureSource);
CapturedImage.Fill = videoBrush;
this.Snapshots.ItemsSource = snaps;
captureSource.Start();
}


private void Snapshot_Click(object sender, RoutedEventArgs e)
{
captureSource.AsyncCaptureImage((image) => { snaps.Add(image); });
}

Five Minute Silverlight 4 Aides-Memoire #2 – Printing

private void Print_Click(object sender, RoutedEventArgs e)
{
    PrintDocument printDoc = new PrintDocument();

    // Name that will show in the spooler...
    printDoc.DocumentName = "Directory Tree";
    printDoc.PrintPage += new EventHandler<PrintPageEventArgs>(printDoc_PrintPage);
    printDoc.Print();
}

private void printDoc_PrintPage(object sender, PrintPageEventArgs e)
{
    // Needs a UIElement (visual tree)
    e.PageVisual = DirectoriesTreeView;
    // Setting true will ensure PrintPage is called again for subsequent pages
    e.HasMorePages = false;
}

Monday, November 30, 2009

Five Minute Silverlight 4 Aides-Memoire #1 – Drag and Drop

Build a hierarchical treeview of files and directories dropped from a Windows Explorer window. Requires elevated trust running out of browser to avoid security exceptions when accessing the directory information in the FileInfo objects returned from the e.Data.GetData call.

image



<Grid x:Name="LayoutRoot" AllowDrop="True" Drop="LayoutRoot_Drop">
<
my:TreeView x:Name="DirectoriesTreeView" Margin="50" Width="500" />
</
Grid>

private void LayoutRoot_Drop(object sender, DragEventArgs e)
{
if (!Application.Current.HasElevatedPermissions e.Data == null)
return;

FileInfo[] fileInfos = e.Data.GetData(DataFormats.FileDrop) as FileInfo[];
if (fileInfos == null)
return;
List<FileInfo> sortedList = new List<FileInfo>(fileInfos);

sortedList.Sort((x, y) =>
{
if (x.Attributes == FileAttributes.Directory && y.Attributes != FileAttributes.Directory)
return -1;
if (y.Attributes == FileAttributes.Directory && x.Attributes != FileAttributes.Directory)
return 1;
return x.Name.CompareTo(y.Name);
});

foreach (FileInfo fileInfo in sortedList)
{
var newNode = new TreeViewItem { Header = fileInfo.Name };
DirectoriesTreeView.Items.Add(newNode);
if (!fileInfo.Exists)
ProcessDirectory(fileInfo, newNode);
}
}
private static void ProcessDirectory(FileSystemInfo fileSystemInfo, TreeViewItem currentNode)
{
DirectoryInfo directoryInfo = new DirectoryInfo(fileSystemInfo.FullName);

foreach (FileSystemInfo childFileSystemInfo in directoryInfo.EnumerateFileSystemInfos())
{
var newNode = new TreeViewItem { Header = childFileSystemInfo.Name };
currentNode.Items.Add(newNode);
if (childFileSystemInfo.Attributes == FileAttributes.Directory)
ProcessDirectory(childFileSystemInfo, newNode);
}
}

Wednesday, November 18, 2009

Silverlight 4

Quite unexpectedly, at least I wasn’t expecting it, today saw the announcement of the first beta of Silverlight 4 at PDC09 in Los Angeles. Barely has version 3 made it out there, but we have a new beta to play with together with another rev of Blend to support it (and .NET 4 in general).
A quick run through the main features in the beta:
  • Printing – Full access to the regular print facilities of the host machine – the print dialog and print preview features – so long to all the writable bitmaps and HTML bridge hacks
  • Webcam/microphone support – Subject to UAC/user confirmation, you can now programmatically get access to connected devices, capture video, stills and audio input streams
  • A Rich TextBox control – the WPF RTF control makes its appearance in Silverlight – embed other UI Elements, control text Runs and other formatting such as Text Decorations
  • Clipboard Access – Programmatic access to the clipboard – again subject to UAC
  • Right Mouse Context Menus – Full control of a customisable context menu – add whatever you want to it and respond to selection events
  • Drag-Drop Drop-Target support Silverlight apps can now act as drag-drop drop-targets receiving file information for the dropped file object(s)
  • MVVM Commanding – Finally proper implementation of the ICommand interface – no longer a need to kludge it via bindings to helper classes
  • IDataErrorInfo/Validation support – Integration of the support for the IDataErrorInfo interface in Silverlight’s DataBinding giving a validation framework for input controls to leverage in consistently surfacing data input errors with asynchronous data validation
  • String Formatting in Binding Extensions – As with WPF, Silverlight has been retrofitted with improved formatting support in the binding extension to give better control over stuff like date, time and other formatting for locales and long and short formatting
  • Out of Browser improvements: HTML content - Hosting of HTML content within the Silverlight app; Notifications (aka toast) support; Elevated Trust (again via UAC) access to the My Documents, My Videos, My Pictures special folders; Cross domain access and Full keyboard access in Full Screen mode
  • Better tooling and integration of RIA/ADO Services/WCF/MEF
Lots to check out and try out – exciting stuff!

Tuesday, August 11, 2009

Five Minute Silverlight 3 Aides-Memoire #5 – Based on Styles

    <UserControl.Resources>
<
Style x:Key="ColouredContentControlStyle" TargetType="ContentControl">
<
Setter Property="Height" Value="30" />
<
Setter Property="Width" Value="100" />
<
Setter Property="Margin" Value="10" />
<
Setter Property="FontFamily" Value="Verdana" />
<
Setter Property="FontSize" Value="14" />
<
Setter Property="FontStyle" Value="Italic" />
</
Style>

<
Style x:Key="ColouredCheckBoxStyle" BasedOn="{StaticResource ColouredContentControlStyle}" TargetType="CheckBox">
<
Setter Property="Width" Value="154" />
</
Style>

<
Style x:Key="RedButtonStyle" BasedOn="{StaticResource ColouredContentControlStyle}" TargetType="Button">
<
Setter Property="Foreground" Value="Red" />
</
Style>
<
Style x:Key="GreenButtonStyle" BasedOn="{StaticResource ColouredContentControlStyle}" TargetType="Button">
<
Setter Property="Foreground" Value="Green" />
</
Style>
<
Style x:Key="YellowCheckBoxStyle" BasedOn="{StaticResource ColouredCheckBoxStyle}" TargetType="CheckBox">
<
Setter Property="Foreground" Value="Yellow" />
</
Style>
<
Style x:Key="OrangeRadioButtonStyle" BasedOn="{StaticResource ColouredContentControlStyle}" TargetType="RadioButton">
<
Setter Property="Foreground" Value="Orange" />
<
Setter Property="Width" Value="190" />
<
Setter Property="FontWeight" Value="Bold" />
</
Style>
</
UserControl.Resources>
<
Grid x:Name="LayoutRoot">
<
Grid.Background>
<
LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<
GradientStop Color="Black" Offset="0"/>
<
GradientStop Color="White" Offset="1"/>
</
LinearGradientBrush>
</
Grid.Background>
<
StackPanel Orientation="Vertical" Margin="20">
<
Button Style="{StaticResource RedButtonStyle}" Content="Red Button" />
<
CheckBox Style="{StaticResource YellowCheckBoxStyle}" Content="Yellow CheckBox" />
<
RadioButton Style="{StaticResource OrangeRadioButtonStyle}" Content="Orange RadioButton" />
<
Button Style="{StaticResource GreenButtonStyle}" Content="Green Button" />
</
StackPanel>
</
Grid>


FiveMinuteSilverlight3_5

Five Minute Silverlight 3 Aides-Memoire #4 – Merged Resource Dictionaries

ButtonStyles.xaml:

<ResourceDictionary 
x:Name="ButtonsStyles"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<
Style x:Key="RedButton" TargetType="Button">
<
Setter Property="Foreground" Value="Red" />
<
Setter Property="Height" Value="30" />
<
Setter Property="Width" Value="100" />
<
Setter Property="Margin" Value="10" />
</
Style>
<
Style x:Key="GreenButton" TargetType="Button">
<
Setter Property="Foreground" Value="Green" />
<
Setter Property="Height" Value="30" />
<
Setter Property="Width" Value="100" />
<
Setter Property="Margin" Value="10" />
</
Style>
</
ResourceDictionary>


CheckBoxStyles.xaml:




<ResourceDictionary 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="CheckBoxStyles"
>
<
Style x:Key="YellowCheckBox" TargetType="CheckBox">
<
Setter Property="Foreground" Value="Yellow" />
<
Setter Property="Height" Value="30" />
<
Setter Property="Margin" Value="10" />
</
Style>
<
Style x:Key="OrangeCheckBox" TargetType="CheckBox">
<
Setter Property="Foreground" Value="Orange" />
<
Setter Property="Height" Value="30" />
<
Setter Property="Margin" Value="10" />
</
Style>
</
ResourceDictionary>


MainPage.xaml:




<UserControl.Resources>
<
ResourceDictionary>
<
ResourceDictionary.MergedDictionaries>
<
ResourceDictionary Source="ButtonStyles.xaml" />
<
ResourceDictionary Source="CheckBoxStyles.xaml" />
</
ResourceDictionary.MergedDictionaries>
</
ResourceDictionary>
</
UserControl.Resources>
<
Grid x:Name="LayoutRoot">
<
Grid.Background>
<
LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<
GradientStop Color="Black" Offset="0"/>
<
GradientStop Color="White" Offset="1"/>
</
LinearGradientBrush>
</
Grid.Background>
<
StackPanel Orientation="Horizontal">
<
Button Grid.Row="0" Grid.Column="0" Style="{StaticResource RedButton}" Content="Red Button" />
<
Button Grid.Row="0" Grid.Column="1" Style="{StaticResource GreenButton}" Content="Green Button" />
<
CheckBox Grid.Row="1" Grid.Column="0" Style="{StaticResource YellowCheckBox}" Content="Yellow CheckBox" />
<
CheckBox Grid.Row="1" Grid.Column="1" Style="{StaticResource OrangeCheckBox}" Content="Orange CheckBox" />
</
StackPanel>
</
Grid>

FiveMinuteSilverlight3_4

Thursday, August 06, 2009

Five Minute Silverlight 3 Aides-Memoire #3 – Element-to-element binding

    <Grid x:Name="LayoutRoot">
<
Grid.RowDefinitions>
<
RowDefinition Height="*" />
<
RowDefinition Height="40" />
</
Grid.RowDefinitions>
<
Grid.ColumnDefinitions>
<
ColumnDefinition />
<
ColumnDefinition />
</
Grid.ColumnDefinitions>
<
Grid.Background>
<
LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<
GradientStop Color="Black" Offset="0"/>
<
GradientStop Color="White" Offset="1"/>
</
LinearGradientBrush>
</
Grid.Background>
<
Image Grid.Column="0" HorizontalAlignment="Left" Margin="50,50,0,0" Width="200" Height="200" Source="OpenfeatureLizardSquare.jpg">
<
Image.Projection>
<
PlaneProjection x:Name="ImagePlaneProjection"/>
</
Image.Projection>
</
Image>
<
Slider Value="{Binding RotationX, Mode=TwoWay, ElementName=ImagePlaneProjection}" Grid.Row="1" Grid.Column="0" x:Name="XSlider" Minimum="-360" Maximum="360" Margin="50,10" />
<
Slider Value="{Binding RotationY, Mode=TwoWay, ElementName=ImagePlaneProjection}" Grid.Row="1" Grid.Column="1" x:Name="YSlider" Minimum="-360" Maximum="360" Margin="50,10" />
</
Grid>

FiveMinuteSilverlight3_3

Wednesday, August 05, 2009

Five Minute Silverlight 3 Aides-Memoire #2 – Pixel Effects

    <Grid x:Name="LayoutRoot">
<
Grid.ColumnDefinitions>
<
ColumnDefinition />
<
ColumnDefinition />
</
Grid.ColumnDefinitions>
<
Grid.Background>
<
LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<
GradientStop Color="Black" Offset="0"/>
<
GradientStop Color="White" Offset="1"/>
</
LinearGradientBrush>
</
Grid.Background>
<
Image HorizontalAlignment="Left" Grid.Column="0" Margin="50,50,0,0" Width="200" Height="200" Source="OpenfeatureLizardSquare.jpg">
<
Image.Effect>
<
BlurEffect Radius="15"/>
</
Image.Effect>
</
Image>
<
Image HorizontalAlignment="Left" Grid.Column="1" Margin="50,50,0,0" Width="200" Height="200" Source="OpenfeatureLizardSquare.jpg">
<
Image.Effect>
<
DropShadowEffect ShadowDepth="10"/>
</
Image.Effect>
</
Image>
</
Grid>

FiveMinuteSilverlight3_2

Five Minute Silverlight 3 Aides-Memoire #1 – 3D Projection

    <Grid x:Name="LayoutRoot">
<
Grid.Background>
<
LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<
GradientStop Color="Black" Offset="0"/>
<
GradientStop Color="White" Offset="1"/>
</
LinearGradientBrush>
</
Grid.Background>
<
Image HorizontalAlignment="Left" Margin="50,50,0,0" Width="200" Height="200" Source="OpenfeatureLizardSquare.jpg">
<
Image.Projection>
<
PlaneProjection RotationX="-25" RotationY="45"/>
</
Image.Projection>
</
Image>
</
Grid>

FiveMinuteSilverlight3_1

Wednesday, June 10, 2009

3D projections in Silverlight 3 Beta – part 2

In my previous post on 3D projections in Silverlight 3 Beta, I created a basic PhotoCube that had images on the faces of a rotating cube created using the new 2D/3D PlaneProjection capabilities available in Silverlight 3.

BasicCube BasicPhotoCube

I thought I’d do a bit more and add some animation to the cube’s rotation and also do away with the slider and rotate the cube using the mouse. I thought it would be nice if the cube rotated slowly all the time, regardless of any mouse interaction, so the first thing to do was to set up an animation to do that. Because I want to be able to manipulate this animation whilst the various mouse events are firing to allow me to control of the cube’s rotation independent of the ‘background’ rotation, I need to define it at the page level:

private Storyboard slowStoryboard;
readonly Dictionary<Image, double> yRotations = new Dictionary<Image, double>();
private double mouseStart;
private bool mouseIsDown;

public PhotoCubePage()
{
InitializeComponent();

SetUpRotations();
slowStoryboard = CreateSlowRotation();
slowStoryboard.Begin();
}








private Storyboard CreateSlowRotation()
{
Storyboard newStoryboard = new Storyboard { Duration = new Duration(TimeSpan.FromSeconds(15)) };
newStoryboard.RepeatBehavior = RepeatBehavior.Forever;








    foreach (var imageRotation in yRotations)
{
var animation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(15)), BeginTime = new TimeSpan(0) };
newStoryboard.Children.Add(animation);
Storyboard.SetTarget(animation, (imageRotation.Key.Projection as PlaneProjection));
Storyboard.SetTargetProperty(animation, new PropertyPath("(PlaneProjection.RotationY)"));
animation.To = imageRotation.Value + 360;
}

return newStoryboard;
}








private void SetUpRotations()
{
foreach (Image image in ImageGrid.Children.Cast<Image>())
{
yRotations.Add(image, ((PlaneProjection)image.Projection).RotationY);
image.MouseLeftButtonDown += ImageMouseLeftButtonDown;
image.MouseLeftButtonUp += ImageMouseLeftButtonUp;
image.MouseMove += ImageMouseMove;
}
}











The animation is performed on each image as it will be for the mouse control, but a 15 second duration makes the cube turn slowly and I want a full rotation of 360 degrees. Setting the RepeatBehavior to Forever ensures that the animation doesn’t stop when it completes a complete rotation. The setup of the Dictionary of Images and their initial RotationY values has been moved to a method to tidy it up, and I now hook up the mouse events here as I need to respond to these on each image surface.









In the mouse down event I record the X-position of the mouse, pause the slow-moving animation and capture the mouse. In the mouse move event, I calculate the mouse movement in the Y-direction and do an rough calculation based on the width of a face of the cube and the amount of mouse movement, to decide on the animation of the faces of the cube which I feed into the cube animation method. In the mouse up event I release mouse capture and restart the slow rotation of the cube.









private void ImageMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mouseStart = e.GetPosition(this).X;
slowStoryboard.Pause();
mouseIsDown = ((UIElement)sender).CaptureMouse();
}

private void ImageMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
mouseIsDown = false;
ReleaseMouseCapture();
slowStoryboard = CreateSlowRotation();
slowStoryboard.Begin();
}

private void ImageMouseMove(object sender, MouseEventArgs e)
{
if (mouseIsDown)
{
double mouseMovement = mouseStart - e.GetPosition(this).X;
double rotation = mouseMovement / FrontImage.Width * 90;
AnimateRotation(rotation, 1);
}
}

private void AnimateRotation(double movement, double seconds)
{
var duration = new Duration(TimeSpan.FromSeconds(seconds));
var storyboard = new Storyboard { Duration = duration };

foreach (var imageRotation in yRotations)
{
var animation = new DoubleAnimation { Duration = duration, BeginTime = new TimeSpan(10) };
storyboard.Children.Add(animation);
Storyboard.SetTarget(animation, (imageRotation.Key.Projection as PlaneProjection));
Storyboard.SetTargetProperty(animation, new PropertyPath("(PlaneProjection.RotationY)"));
animation.To = ((PlaneProjection)imageRotation.Key.Projection).RotationX + imageRotation.Value + movement;
animation.EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 3, Springiness = 6 };
}

storyboard.Begin();
}











Just so that all the code is shown, here is the fairly simple XAML for the page:









    <Grid x:Name="LayoutRoot" Background="DarkGray">
<
Grid x:Name="ImageGrid" >
<
Image x:Name="FrontImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/silverlight.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="0"/>
</
Image.Projection>
</
Image>
<
Image x:Name="LeftImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/OpenfeatureLizardSquare.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="90"/>
</
Image.Projection>
</
Image>
<
Image x:Name="BackImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/Waterfall.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="180"/>
</
Image.Projection>
</
Image>
<
Image x:Name="RightImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/Tulip.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="270"/>
</
Image.Projection>
</
Image>
</
Grid>
</
Grid>











 









The result is that the cube turns slowly whilst the mouse is not being used to manipulate it and while the mouse is used to drag it the cube responds with animated movement; I use one of the pre-canned easing functions to give the animation a ‘springy’ feeling at the end of each movement. Notice that the animation method calculates the move-to value for each animation by adding the original position of the image/face to the movement value AND the current value of the RotationY which has been affected by the slow rotation.









And here’s the result:




















You may have noticed that although I have called the thing I am rotating a cube and in the first post there were actually six images, I have done away with the top and bottom of my cube in the latest mark-up. This is because they are not really visible whilst the ‘cube’ is rotating on just the Y-axis; I had intended to make it a proper cube and allow rotation along 2 axes (which would have given me full articulation of the cube, or even 3 – to allow me to move the cube in and out of the screen), but there is a problem when it comes to allowing movement in more that one direction.









The problem lies in the fact that once an image plane has been rotated along one axis, rotation in another one happens in relation to the original position, not the new one, so that the faces that become the sides of the cube, the back of the cube, the top and the bottom all need to be transformed in different ways to maintain the illusion of a solid shape in 3D space. For example, after the original Y-axis rotation to setup the cube’s sides and the X-axis rotation to setup the cube’s top and bottom, to rotate the cube along the X-axis, the sides need to be rotated along the Z-axis (i.e. the one that runs through their centre) and this assumes that they aren’t transformed further in the Y-axis themselves, by a slider or my attempts at animating them with mouse movement. In fact the maths for calculating the positions of the faces of the cube are quite complex; from dipping into Charles Petzold’s book: 3D Programming for Windows and quickly getting bogged down with quaternions and rotation matrices, it was clear the maths were beyond the scope of this post, so I chickened out and did away with the top and bottom of the cube and abandoned the notion of full articulation (for now). So we have ended up what might be better called a ‘PhotoPrism’ that rotates only along the Y-axis.









This new name did give me the idea for a further refinement of the cube: if the thing is a prism, there is no reason why it can’t have a variable number of faces, from 3 upwards – the maths for calculating the positioning and angles of rotation for the faces would be much easier.









Hmm, watch this space…