|
This post is a follow-up to the following two articles:
Rotating the Camera in WPF
3D Hit Testing with WPF
A lot of people are doing some really great things with WPF. On the other hand, a lot of what I've seen so far would fall under Alan Cooper's categorization of a "painted corpse".
Slapping gradients and spinning rotating objects into an interface just for the sake of doing it doesn't really add much value to the customer.
Don't get me wrong: it's fabulous that this kind of technology is now commoditized for us in the form of WPF, and no longer the exclusive domain of "Direct3D Programmers",
but we can easily abuse this technology. A friend of mine once told me that the .NET Framework was like taking the little deringer that used to be COM
and converting it into a .44 magnum. My response was "So now we have a larger gun with which to shoot ourselves in the foot."
The more power our tools give us, the more we need to be aware of abusing that power and abusing the people who use our applications.
One of the things I've always been interested in is presenting data in new and more useful ways. I'm pretty tired of the same old story: "Got data? ok, slap a DataGrid on it and call it done.". It's getting pretty sad and pretty tired. When you think about it - programmers are really the only kinds of people who just love having their data displayed to them in DataGrids. In this post, I'm going to play around with some WPF 3D techniques as well as some 2D techniques to get data to appear as a result of clicking an object in a 3D space. My proof of concept is actually a space game, and the cubes represent star ports (they will eventually be more complex meshes, but I wanted the interaction fleshed out before spending time modelling). When you click a star port, the final game will actually go out and communicate with the player that is hosting that star port (via WCF self-hosted services) and grab information on the port such as an image for the port, the port's name, description, how many players are docked at it, etc. This doesn't have to be a gaming-only concept. You could easily devise some scheme whereby the parent information is laid out in 3D meshes in an immersive environment. When you click the parent node/object/mesh/whatever, the client then fetches the raw detail information and then displays it in a stylish 2D panel.
To get started, the first thing we need is a data source. There are a dozen different ways you can create a data source in WPF, but I only need one object (not a list of objects), and I've only got one instance of that object (the currently selected starport cube), so I'm going to use an ObjectDataProvider. You can set one up in your XAML by first creating an XML namespace mapping to a CLR namespace with the following line of XAML:
<Window x:Class="Cube1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:DataLibrary;assembly=DataLibrary"
....
....
This essentially now gives me the freedom to refer to any class contained in the DataLibrary namespace in the DataLibrary Assembly directly within my XAML. I can't even begin to touch on how ridiculously useful this feature is, especially for those of us used to making really large apps with multiple libraries. Next, you need to create the ObjectDataProvider, which can be in any .Resources element. In this case, I chose Window.Resources to make it available to the entire window:
<ObjectDataProvider x:Key="odpPort">
</ObjectDataProvider>
Not much here, right? Well, I could choose to specify the data type of the underlying object data source, in which case the data provider would then take care of creating the instance for me. I don't want that: I want this object data provider to point to a member variable that is part of the Window's code-behind, so I'll rig that up programmatically. First, add a member variable of type ObjectDataProvider:
private ObjectDataProvider odp;
private DataLibrary.PortData currentPort = new DataLibrary.PortData();
Then, at the end of the Window_Loaded event handler, set the ObjectInstance property of the data provider to the currentPort member:
odp = (ObjectDataProvider)this.FindResource("odpPort");
currentPort.PortName = "-- No Port --";
odp.ObjectInstance = currentPort;
There's one more thing that needs to be done in order for this whole thing to work properly. Custom classes that you write will not be able to inform the WPF GUI subsystem that properties have changed unless those objects implement the INotifyPropertyChanged interface. Here's a listing of the DataLibrary.PortData class:
namespace DataLibrary
{
public class PortData : INotifyPropertyChanged
{
private string portName;
private string image;
private string longDesc;
public string PortName
{
get { return portName; }
set { portName = value; OnPropertyChanged("PortName"); }
}
public string LongDescription
{
get { return longDesc; }
set { longDesc = value; OnPropertyChanged("LongDescription"); }
}
public string Image
{
get { return image; }
set { image = value; OnPropertyChanged("Image"); }
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
#endregion
}
}
Now that the data-bound class is able to communicate property changes to the GUI, we're ready to rig up a panel that will display the information obtained by clicking on a cube. In my sample, I'm fabricating the information rather than really getting it from a WCF service just to keep things easy to read. The following is the XAML for the DockPanel that contains the port information:
<!-- PORT VIEWER -->
<DockPanel Visibility="Hidden" x:Name="dpPortInfo" Width="0" Height="300" VerticalAlignment="Top" Margin="2">
<DockPanel.DataContext>
<Binding Source="{StaticResource odpPort}"></Binding>
</DockPanel.DataContext>
<DockPanel.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Color="DarkBlue" Offset="0"/>
<GradientStop Color="LightBlue" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</DockPanel.Background>
<DockPanel.BitmapEffect>
<DropShadowBitmapEffect Color="AliceBlue"></DropShadowBitmapEffect>
</DockPanel.BitmapEffect>
<Grid Width="180" ShowGridLines="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<Border BorderBrush="Silver" CornerRadius="5" Width="110" Grid.Column="0" Grid.Row="0">
<Rectangle Width="100" Height="100">
<Rectangle.Fill>
<ImageBrush x:Name="imgPortIcon" ImageSource="{Binding Image}"></ImageBrush>
</Rectangle.Fill>
</Rectangle>
</Border>
<TextBlock Text="{Binding PortName}" Foreground="Yellow" Margin="5" Grid.Column="0" Grid.Row="1"></TextBlock>
<TextBlock Text="{Binding LongDescription}" Foreground="White" Margin="5" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2"></TextBlock>
</Grid>
</DockPanel>
The fact that its a completely mindless task to add a colored drop-shadow to any WPF element is just pretty staggering to me. I love little details like that.
There's one more thing before I rig up the hit testing code. I want to animate the expansion of the port info panel with a storyboard, so I'll put a storyboard in the Window.Resources element as shown below:
<Storyboard Storyboard.TargetName="dpPortInfo" Storyboard.TargetProperty="Width" x:Key="sbWidthExpander">
<DoubleAnimation From="0" To="170" Duration="0:0:00.5"></DoubleAnimation>
</Storyboard>
This storyboard will animate the Width property from 0 to 170 in half a second. Its a pretty decent window expansion time - anything longer than that and users are going to be tapping their fingers or, worse, they might be wondering why your app is running so slow.
Now take a look at the hit-test code from the previous blog. The difference is that this time I have added code that locates the width-expansion storyboard and activates it, and changes values on the currentPort object instance. Note that I never have to re-set the ObjectInstance property or do any refreshing of the data source. The GUI is immediately aware of changes to my data-bound object!
public HitTestResultBehavior HTResult(System.Windows.Media.HitTestResult rawresult)
{
RayHitTestResult rayResult = rawresult as RayHitTestResult;
if (rayResult != null)
{
RayMeshGeometry3DHitTestResult rayMeshResult = rayResult as RayMeshGeometry3DHitTestResult;
if (rayMeshResult != null)
{
GeometryModel3D hitgeo = rayMeshResult.ModelHit as GeometryModel3D;
tbHitCheck.Text = "You hit mesh (" + meshes[hitgeo] + ") at " + DateTime.Now.ToString();
dpPortInfo.Visibility = Visibility.Visible;
currentPort.PortName = meshes[hitgeo];
currentPort.Image = @"C:\documents and settings\kevin\my documents\my pictures\penfold.jpg";
currentPort.LongDescription = "Star port floating in space.";
Storyboard s = (Storyboard)this.FindResource("sbWidthExpander");
this.BeginStoryboard(s);
}
}
return HitTestResultBehavior.Continue;
}
The only thing that irked me so far about data binding is that I can't seem to bind an in-memory, loaded Image to the ImageSource property of a brush. Oh well - small sacrifice to pay I suppose. Now we'll finish this whole thing off with a screenshot of what the application looks like with its fresh new data visualization panel:

I just stumbled onto this post and I was pretty excited because I have been
planning a
spiritual successor to the old BBS game, Trade Wars, for some time. My
partners in the effort and I had only recently decided to abandon Flash as
our client platform in favor of WPF (a more limited audience, but much
easier to manage).
Any one got a solution for this problem ?