I wanted to format a TimeSpan in XAML and differentiate it from a normal time format, something like this 02h30m
After a bit of fiddling with curly brackets and back slashes I settled on:
"{Binding Path=Duration, StringFormat={}{0:hh}h{0:mm}m}"
One step forward, two steps back
"{Binding Path=Duration, StringFormat={}{0:hh}h{0:mm}m}"
public StudentCourseSearchView()
{
InitializeComponent();
SearchResults.Sorting += new DataGridSortingEventHandler(SearchResults_Sorting);
}
Then in the private SearchResults_Sorting method I determine if it is the StudentID column that is being sorted and instantiate the custom sorting class.
void SearchResults_Sorting(object sender, System.Windows.Controls.DataGridSortingEventArgs e)
{
DataGridColumn column = e.Column;
// I'm only interested in a custom sort for the StudentID column
if (column.SortMemberPath != "StudentID.Number") return;
IComparer comparer = null;
// Prevent the built-in sort from sorting
e.Handled = true;
ListSortDirection direction = (column.SortDirection != ListSortDirection.Ascending) ? ListSortDirection.Ascending : ListSortDirection.Descending;
// Set the sort order on the column
column.SortDirection = direction;
//use a ListCollectionView to do the sort.
ListCollectionView lcv = (ListCollectionView)CollectionViewSource.GetDefaultView(TypedViewModel.Students);
// Instantiate the custom sort class which implements IComparer
comparer = new StudentSearchResultStudentIdSort(direction);
// Apply the sort
lcv.CustomSort = comparer;
}
Finally, the StudentSearchResultStudentIdSort class implements the Compare method using the SortableStringValue extension method.
public class StudentSearchResultStudentIdSort : IComparer
{
ListSortDirection _direction;
public StudentSearchResultStudentIdSort(ListSortDirection direction)
{
_direction = direction;
}
public int Compare(object x, object y)
{
string studentIdX = (x as StudentSearchResult).StudentID.Value;
string studentIdY = (y as StudentSearchResult).StudentID.Value;
if (_direction == ListSortDirection.Ascending)
{
return studentIdX.SortableStringValue().CompareTo(studentIdY.SortableStringValue());
}
else
{
return studentIdY.SortableStringValue().CompareTo(studentIdX.SortableStringValue());
}
}
}
<Rectangle
Stroke="Black">
<Rectangle.Style>
<Style TargetType="{x:Type Rectangle}">
<Setter Property="StrokeThickness"
Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Selected}"
Value="True">
<Setter Property="StrokeThickness"
Value="2" />
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
public List<string> SimpleColorList
{
get
{
List<string> colorList = new List<string>();
foreach (PropertyInfo pi in typeof(Colors).GetProperties())
{
colorList.Add(pi.Name);
}
return colorList;
}
}
And bound it to a ComboBox. Using Mode=OneTime means the ColorList is only built once.<ComboBox ItemsSource="{Binding Path=SimpleColorList, Mode=OneTime}"
SelectedValue="{Binding SelectedColor}"
Width="200" />
Adding a ItemTemplate to the ComboBox makes it a little more interesting:<ComboBox ItemsSource="{Binding Path=SimpleColorList, Mode=OneTime}"
SelectedValue="{Binding SelectedColor}"
Width="200">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width="130"
Text="{Binding}" />
<Border BorderBrush="Black"
BorderThickness="1"
CornerRadius="4"
Margin="0,1,0,1"
Background="{Binding}"
Width="40"
Height="18">
</Border>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The straight alphabetic sorted list is fine but I wanted to sort by "color", in general terms from lighter to darker. I found some code on the web that would convert the RGB into the HSL color space. (I'm afraid I can't find the code again so my apologies to its author for the lack of attribution). public static class HslValueConverter
{
/// <summary>
/// Converts a WPF RGB color to an HSL color
/// </summary>
/// <param name="rgbColor">The RGB color to convert.</param>
/// <returns>An HSL color object equivalent to the RGB color object passed in.</returns>
public static HslColor RgbToHsl(string name, Color rgbColor)
{
// Initialize result
var hslColor = new HslColor();
hslColor.Name = name;
// Convert RGB values to percentages
double r = (double)rgbColor.R / 255;
var g = (double)rgbColor.G / 255;
var b = (double)rgbColor.B / 255;
var a = (double)rgbColor.A / 255;
// Find min and max RGB values
var min = Math.Min(r, Math.Min(g, b));
var max = Math.Max(r, Math.Max(g, b));
var delta = max - min;
/* If max and min are equal, that means we are dealing with
* a shade of gray. So we set H and S to zero, and L to either
* max or min (it doesn't matter which), and then we exit. */
//Special case: Gray
if (max == min)
{
hslColor.Hue = 0;
hslColor.Saturation = 0;
hslColor.Lightness = max;
return hslColor;
}
/* If we get to this point, we know we don't have a shade of gray. */
// Set L
hslColor.Lightness = (min max) / 2;
// Set S
if (hslColor.Lightness < 0.5)
{
hslColor.Saturation = delta / (max min);
}
else
{
hslColor.Saturation = delta / (2.0 - max - min);
}
// Set H
if (r == max) hslColor.Hue = (g - b) / delta;
if (g == max) hslColor.Hue = 2.0 (b - r) / delta;
if (b == max) hslColor.Hue = 4.0 (r - g) / delta;
hslColor.Hue *= 60;
if (hslColor.Hue < 0) hslColor.Hue = 360;
// Set A
hslColor.Alpha = a;
// Set return value
return hslColor;
}
}
public struct HslColor
{
public string Name { get; set; }
public double Alpha;
public double Hue;
public double Lightness;
public double Saturation;
}
Then I changed the property (Note: the property name has changed to SortedColorList, so the ComboBox Binding will have to be altered) "getter" to sort by Saturation, Hue and Lightness. I also decided to exclude the Transparent color. public List<string> SortedColourList
{
get
{
List<HslColor> colorList = new List<HslColor>();
foreach (PropertyInfo pi in typeof(Colors).GetProperties())
{
Color color = (Color)pi.GetValue(null, null);
// Only select non-Transparent colors
if (color.A != 0) colorList.Add(HslValueConverter.RgbToHsl(pi.Name, color));
}
return colorList.OrderBy(c => c.Saturation)
.OrderBy(c => c.Hue)
.OrderByDescending(c => c.Lightness)
.Select(c => c.Name).ToList<string>();
}
}
The SimpleColorPicker project is available on GoogleCode.
<DataTemplate DataType="{x:Type dts:BookingLineItem}">
<DataTemplate DataType="{x:Type dts:CCardPaymentLineItem}">
<DataTemplate DataType="{x:Type dts:CashPaymentLineItem}">
<DataTemplate DataType="{x:Type dts:LineItem}">
There is no need to define the DataTemplateSelector in the resources, no C# class to write and the ListBox becomes simpler: <ListBox Name="theSecondTillRoll"
Height="150"
Width="330"
ItemsSource="{Binding SaleItems}" />
The DataTemplateSelector project is on GoogleCode and demonstrates both techniques. <TextBlock>
<Hyperlink Click="Hyperlink_Click"
NavigateUri="http://mikestedman.blogspot.com/2012/03/wpf-string-formatting.html">Link to original WPF Waltz blog entry</Hyperlink>
</TextBlock>
And a simple event handler in the code-behind: private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
Process.Start((sender as Hyperlink).NavigateUri.ToString());
}
public partial class MainWindow : Window
{
public String FormattedDate
{
get { return String.Format("{0:dd MMM yyy}", DateTime.Now); }
}
public String DateToString
{
get { return DateTime.Now.ToString("dd MMM yyy"); }
}
public DateTime RawDateTime
{
get { return DateTime.Now; }
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
The first two TextBlocks bind directly to the pre-formatted properties. <TextBlock Text="{Binding FormattedDate}" />
<TextBlock Text="{Binding DateToString}" />
Or we can bind to the raw data and use StringFormat. We can escape the curly braces in a couple of ways. <TextBlock Text="{Binding Path=RawDateTime, StringFormat=\{0:dd MMM yyyy\}}" />
<TextBlock Text="{Binding Path=RawDateTime, StringFormat={}{0:dd MMM yyyy}}" />
The leading curly braces in the second version can be replaced by constant text that is rendered as part of the formatted string. But it seems a bit "sensitive" so I don't use it. <TextBlock Text="{Binding Path=RawDateTime, StringFormat=Today is {0:dd MMM yyyy}}" />
Next up is a Converter for which we need an IValueConverter class:class DateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime)
{
return ((DateTime)value).ToString("dd MMM yyy");
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And the corresponding XAML to use the Converter: xmlns:local="clr-namespace:WpfWaltz"
...
<Window.Resources>
<local:DateConverter x:Key="dateConverter"; />
</Window.Resources>
...
<TextBlock Text="{Binding Path=RawDateTime, Converter={StaticResource dateConverter}}" />
Finally, I had assumed that the indexer in the StringFormat was just a necessary evil since it was always 0: and hadn't considered using StringFormat with a MultiBinding like this: <TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:dd MMM yyyy} {1:hh:mm:ss}">
<Binding Path="RawDateTime" />
<Binding Path="RawDateTime" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
The StringFormatting project that has all the code can be downloaded from GoogleCode.
public partial class DataGridProgressBar : UserControl
{
public readonly static DependencyProperty LoadingMessageProperty =
DependencyProperty.Register(
"LoadingMessage",
typeof(string),
typeof(DataGridProgressBar));
public readonly static DependencyProperty EmptyDatasetMessageProperty =
DependencyProperty.Register(
"EmptyDatasetMessage",
typeof(string),
typeof(DataGridProgressBar));
public string LoadingMessage
{
get { return (string)GetValue(LoadingMessageProperty); }
set { SetValue(LoadingMessageProperty, (string)value); }
}
public string EmptyDatasetMessage
{
get { return (string)GetValue(EmptyDatasetMessageProperty); }
set { SetValue(EmptyDatasetMessageProperty, (string)value); }
}
}
Next the user control needs to know the state of the dataset loading and the number of records in the dataset. public readonly static DependencyProperty RecordCountProperty =
DependencyProperty.Register(
"RecordCount",
typeof(long),
typeof(DataGridProgressBar));
public readonly static DependencyProperty LoadInProgressProperty =
DependencyProperty.Register(
"LoadInProgress",
typeof(bool?),
typeof(DataGridProgressBar));
public long RecordCount
{
get { return (long)GetValue(RecordCountProperty); }
set { SetValue(RecordCountProperty, (long)value); }
}
public bool? LoadInProgress
{
get { return (bool?)GetValue(LoadInProgressProperty); }
set { SetValue(LoadInProgressProperty, (bool?)value); }
}
Next we turn our attention to the XAML for the new User Control, remembering to add a namespace reference:<UserControl x:Class="DataGridWatermark.DataGridProgressBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DataGridWatermark">
The actual control consists of just a single, named, TextBlock:<UserControl .......
<TextBlock x:Name="PART_Message" />
</UserControl>
All the hard work is done in the Style which has three main parts: <Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility"
Value="Collapsed" />
<Setter Property="Foreground"
Value="Red" />
<Setter Property="Text"
Value="{Binding LoadingMessage}" />
...
</Style>
We use a DataTrigger to detect the data loading state by binding to the LoadInProgress property and changing the Visibility from Collapsed to Visible. <Style.Triggers>
<DataTrigger Binding="{Binding LoadInProgress}"
Value="true">
<Setter Property="Visibility"
Value="Visible" />
</DataTrigger>
...
</Style.Triggers>
The empty dataset is a little more complicated because there are two conditions that need to be satisfied before the Empty Dataset message is displayed. The two conditions are specified inside a MultiDataTrigger. <MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding RecordCount}"
Value="0" />
<Condition Binding="{Binding LoadInProgress}"
Value="false" />
</MultiDataTrigger.Conditions>
<Setter Property="Foreground"
Value="Silver" />
<Setter Property="Text"
Value="{Binding EmptyDatasetMessage}" />
<Setter Property="Visibility"
Value="Visible" />
</MultiDataTrigger>
The final piece of the jigsaw is providing a DataContext for the property bindings. I chose to explicitly set the DataContext in the code-behind in the constructor.public DataGridProgressBar()
{
InitializeComponent();
PART_Message.DataContext = this;
}
And finally we can add it to the original DataGrid.<Window x:Class="DataGridWatermark.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc="clr-namespace:DataGridWatermark"
...
<DataGrid>
<DataGrid.Background>
<VisualBrush Stretch="None">
<VisualBrush.Visual>
<StackPanel>
<uc:DataGridProgressBar LoadingMessage="Loading..."
EmptyDatasetMessage="No Records Found"
RecordCount="{Binding Path=TheRecordCount}"
LoadInProgress="{Binding Path=TheLoadState}" />
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</DataGrid.Background>
...
</Window>
<DataGrid ItemsSource="{Binding Source{StaticResource ViewModel},Path=EnrolledStudents}"
CanUserAddRows="False"
CanUserDeleteRows="False"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Background>
<VisualBrush Stretch="None">
<VisualBrush.Visual>
<StackPanel Background="White">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10pt"
Margin="2"
Foreground="Red"
Text="{Binding Path=Strings.V_LabelLoading}"
Visibility="{Binding Source{StaticResource ViewModel}, Path=EnrolledStudentLoadingInProgress, Converter={StaticResource boolToCollapsedVisibilityConverter}}" />
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10pt"
Margin="2"
Foreground="Silver"
Text="{Binding Path=Strings.V_LabelNoEnrolledStudents}"
Visibility="{Binding Source{StaticResource ViewModel}, Path=NoEnrolledStudents, Converter={StaticResource boolToCollapsedVisibilityConverter}}" />
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</DataGrid.Background>
<DataGrid.Columns>
<ItemsControl>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
And in C#, working from the deepest levels outward, like this:
FrameworkElementFactory factoryPanel =
new FrameworkElementFactory(typeof(StackPanel));
factoryPanel.SetValue(
StackPanel.OrientationProperty,
Orientation.Horizontal);
ItemsPanelTemplate template = new ItemsPanelTemplate();
template.VisualTree = factoryPanel;
ItemsControl itemsControl = new ItemsControl();
itemsControl.ItemsPanel = template;
<Ellipse
Width="20"
Height="20"
Canvas.Left="20"
Canvas.Top="0"
Stretch="Fill"
Stroke="Black"
Fill="Red">
<Ellipse.Clip>
<RectangleGeometry Rect="0,10,20,20"/>
</Ellipse.Clip>
</Ellipse>
And here is a fuller example with all four semi-circle rotations:<Canvas VerticalAlignment="Center">
<Ellipse
Width="20"
Height="20"
Canvas.Left="20"
Canvas.Top="0"
Stretch="Fill"
Stroke="Black"
Fill="Red">
<Ellipse.Clip>
<RectangleGeometry Rect="0,10,20,20"/>
</Ellipse.Clip>
</Ellipse>
<Ellipse
Width="20"
Height="20"
Canvas.Left="60"
Canvas.Top="0"
Stretch="Fill"
Stroke="Black"
Fill="Red">
<Ellipse.Clip>
<RectangleGeometry Rect="0,0,10,20"/>
</Ellipse.Clip>
</Ellipse>
<Ellipse
Width="20"
Height="20"
Canvas.Left="100"
Canvas.Top="0"
Stretch="Fill"
Stroke="Black"
Fill="Red">
<Ellipse.Clip>
<RectangleGeometry Rect="10,0,10,20"/>
</Ellipse.Clip>
</Ellipse>
<Ellipse
Width="20"
Height="20"
Canvas.Left="140"
Canvas.Top="0"
Stretch="Fill"
Stroke="Black"
Fill="Red">
<Ellipse.Clip>
<RectangleGeometry Rect="0,10,20,20"/>
</Ellipse.Clip>
</Ellipse>
</Canvas>
Which gives this result:<DataGridTextColumn Header="Blah" Binding="{Binding Blah}">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="TextAlignment" Value="Right" />
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
Alternatively the style can be set up as StaticResource if it needs to be referenced by several DataGrid columns.
<Style x:Key="rightAlignedColumn" TargetType="{x:Type TextBlock}">
<Setter Property="TextAlignment" Value="Right" />
</Style>
...
<DataGridTextColumn Header="Blah"
Binding="{Binding Blah}"
ElementStyle="{StaticResource rightAlignedColumn}">
</DataGridTextColumn>
<RadioButton ssform:FormItem.LabelContent="{Binding Source={StaticResource ViewModel}, Path=Strings.V_Coupon, Mode=OneTime}"
Content="{Binding Source={StaticResource ViewModel}, Path=Strings.V_eCoupon, Mode=OneTime}"
IsEnabled="{Binding Source={StaticResource ViewModel}, Path=IsCouponATrigger}"
IsChecked="{Binding Source={StaticResource ViewModel}, Path=IsECouponATrigger, Mode=TwoWay}" />
<RadioButton Content="{Binding Source={StaticResource ViewModel}, Path=Strings.V_PrintCoupon, Mode=OneTime}"
IsEnabled="{Binding Source={StaticResource ViewModel}, Path=IsCouponATrigger}"
IsChecked="{Binding Source={StaticResource ViewModel}, Path=IsPrintedCouponATrigger, Mode=TwoWay}" />
public bool IsPrintedCouponATrigger
{
get { return PromoBase.UIChoices.IsPrintedCouponATrigger; }
set
{
if (value != PromoBase.UIChoices.IsPrintedCouponATrigger)
{
PromoBase.UIChoices.IsPrintedCouponATrigger = value;
IsECouponATrigger = !value;
OnPropertyChanged("IsPrintedCouponATrigger");
}
}
}
public bool IsECouponATrigger
{
get { return PromoBase.UIChoices.IsECouponATrigger; }
set
{
if (value != PromoBase.UIChoices.IsECouponATrigger)
{
PromoBase.UIChoices.IsECouponATrigger = value;
IsPrintedCouponATrigger = !value;
OnPropertyChanged("IsECouponATrigger");
}
}
}
Note the check that the value has actually changed in the setter – without this, you get into a nice infinite loop of PropertyChange notifications as each changes the other in turn! <Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, +0.5 0.5, -0.5, +0.5 0.5, 0.5, +0.5 -0.5, 0.5, +0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush>
<VisualBrush.Visual>
<StackPanel>
<TextBlock FontSize="10pt"
Margin="2">Hello, from the Front!</TextBlock>
<Button Margin="2">A Button</Button>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 -0.5, -0.5, 0.5 -0.5, 0.5, 0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>0.5, -0.5, -0.5 0.5, -0.5, 0.5 0.5, 0.5, 0.5 0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
At this point the front of the cube will be transparent and the TextBlock and Button will not appear. We need to add TextCoordinates to map the VisualBrush to the Mesh.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, +0.5 0.5, -0.5, +0.5 0.5, 0.5, +0.5 -0.5, 0.5, +0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
<MeshGeometry3D.TextureCoordinates>0,1 1,1 1,0 0,0</MeshGeometry3D.TextureCoordinates>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush>
<VisualBrush.Visual>
<StackPanel>
<TextBlock FontSize="10pt"
Margin="2">Hello, from the Front!</TextBlock>
<Button Margin="2">A Button</Button>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 -0.5, -0.5, 0.5 -0.5, 0.5, 0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>0.5, -0.5, -0.5 0.5, -0.5, 0.5 0.5, 0.5, 0.5 0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
To apply the texture to the "back" of the we need to "flip" the TextureCoordinates horizontally around the Y axis to match the way we previously changed the TriangleIndices.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
<MeshGeometry3D.TextureCoordinates>1,1 0,1 0,0 1,0</MeshGeometry3D.TextureCoordinates>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush>
<VisualBrush.Visual>
<StackPanel>
<TextBlock FontSize="10pt"
Margin="2">Hello, from the Back!</TextBlock>
<Button Margin="2">A Button</Button>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, +0.5 0.5, -0.5, +0.5 0.5, 0.5, +0.5 -0.5, 0.5, +0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
<MeshGeometry3D.TextureCoordinates>0,1 1,1 1,0 0,0</MeshGeometry3D.TextureCoordinates>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush>
<VisualBrush.Visual>
<StackPanel>
<TextBlock FontSize="10pt"
Margin="2">Hello, from the Front!</TextBlock>
<Button Margin="2">A Button</Button>
</StackPanel>
</VisualBrush.Visual>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 -0.5, -0.5, 0.5 -0.5, 0.5, 0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>0.5, -0.5, -0.5 0.5, -0.5, 0.5 0.5, 0.5, 0.5 0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
And add the second surface exactly the same as the first but with the Z coordinates moved from -0.5 to +0.5.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, +0.5 0.5, -0.5, +0.5 0.5, 0.5, +0.5 -0.5, 0.5, +0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
We can now see two Maroon/Blue surfaces rotating but we always see the same colour for both surfaces. The original surface needs to point "out" from the centre of the cube. The easiest way would appear to be drawing the Triangles in a different order so their Normals are reversed. Swap 0 and 1, swap 2 and 3 in the TriangleIndices list.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, +0.5 0.5, -0.5, +0.5 0.5, 0.5, +0.5 -0.5, 0.5, +0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
Finally, add the remaining two vertical surfaces of the cube. The "left" surface uses the same TriangleIndices as the "front" whereas the "right" surface uses the same TriangleIndices as the "back".<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, +0.5 0.5, -0.5, +0.5 0.5, 0.5, +0.5 -0.5, 0.5, +0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 -0.5, -0.5, 0.5 -0.5, 0.5, 0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>0.5, -0.5, -0.5 0.5, -0.5, 0.5 0.5, 0.5, 0.5 0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>1 0 3 3 2 1</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
</Grid>
We want to rotate the Model around the Y axis, represented by the "1" in the definition of AxisAngleRotation3D.Axis below. Because the Model has a negative Z coordinate (-0.5) the Z component will gradually increase until the angle of rotation is 180 degrees at which point the Z coordinate becomes 0.5. Because the Z coordinate is closer to use the Model will appear larger. <Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D>
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
</Grid>
Starting with an Angle of 0 (zero) degrees gives the same result as previously. Increasing the Angle to more than 90 degrees the reverse side of the Model comes into view.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D>
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>120</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
</Grid>
The rotation can be animated by applying a DoubleAnimation to the RotateTransform.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
<ModelVisual3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D x:Name="Rotate">
<AxisAngleRotation3D.Axis>0,1,0</AxisAngleRotation3D.Axis>
<AxisAngleRotation3D.Angle>0</AxisAngleRotation3D.Angle>
</AxisAngleRotation3D>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</ModelVisual3D.Transform>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
<Grid.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0"
To="360"
BeginTime="0:0:0"
Duration="0:0:4"
Storyboard.TargetName="Rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
</Grid>
<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
Each Mesh position is a triplet of X, Y and Z. Positive X is UP. Positive Y is RIGHT. Positive Z is OUT of the screen.
3 ------------ 2
| |
| |
| |
0 ------------ 1
These form the indices used to cover the surface in triangles. Each triangle is drawn anti-clockwise to keep the "Normals" pointing in the same direction. I'm not going to mention Normals again.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
The model has two surfaces and each can have its own material. The front surface, Material, has been coloured Maroon.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
At this point nothing can be seen on screen because there is no camera.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
</Grid>
At this point we should see a Black square which is the Maroon surface of the model in an unlit scene. Next we add some AmbientLight.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
</Grid>
We should now be able to see a Maroon surface. The Model's reverse surface, BackMaterial, will be coloured Blue.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,-1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
</Grid>
Finally, reposition the camera and the direction it is looking in order to see the other side of the model. We should now see the blue surface.<Grid Background="AliceBlue">
<Viewport3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D>
<MeshGeometry3D.Positions>-0.5, -0.5, -0.5 0.5, -0.5, -0.5 0.5, 0.5, -0.5 -0.5, 0.5, -0.5</MeshGeometry3D.Positions>
<MeshGeometry3D.TriangleIndices>0 1 2 2 3 0</MeshGeometry3D.TriangleIndices>
</MeshGeometry3D>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Maroon</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>Blue</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="White" />
</ModelVisual3D.Content>
</ModelVisual3D>
<Viewport3D.Camera>
<PerspectiveCamera>
<PerspectiveCamera.LookDirection>0,0,1</PerspectiveCamera.LookDirection>
<PerspectiveCamera.Position>0,0,-2</PerspectiveCamera.Position>
<PerspectiveCamera.FieldOfView>90</PerspectiveCamera.FieldOfView>
</PerspectiveCamera>
</Viewport3D.Camera>
</Viewport3D>
</Grid>