MyFriends: A simple contact keeper using XLINQ/LINQ/WPF






4.56/5 (98 votes)
A simple contact keeper using XLINQ/LINQ/WPF.
Contents
- Introduction
- What is in this article
- The demo app
- 3D
- The 3D DataTemplate
- How the Singleton pattern saved the day
- Traversing the visual tree for a templated object
- File system treeview, with lazy loading and icons
- LINQ over objects
- XLINQ usage
- Getting gushy about the Xceed data grid
- Some slightly sneaky value converters
- Vista style dialogs
- References
- So what do you think?
- Conclusion
Introduction
The other week, I was surfing the web and I came across something that perked my interest, and this article is kind of a result of what I found. Basically, I come from a database type of background, so I am used to seeing forms with grids, lists, and listviews which are functional but look pretty boring. This is kind of why I like WPF so much, as you can make functional apps, but make them look really sexy. Anyone that has done any WPF will probably know that there are a couple of data type controls such as ListBox
and ListView
, and with some clever binding and some templating/styling, we could probably make these look like a grid. But the truth is that there simply is no DataGrid or anything like that.
That short fall is kind of what this article is about, but I hope this article will have enough extra meat to keep people interested.
What is in this article
I've just mentioned that this article will feature a DataGrid for WPF, and it will. This is the little gem that I mentioned that I found while trawling the internet. And the best part is that it is totally free and that you get full support for it and even free upgrades. I couldn't believe that, but it's true. I've done some homework, contacted support etc. The DataGrid in question is from Xceed and can be found right here. I have to say Xceed has done a bang up job in my opinion. I mean, I would not be wasting my time (which is Xceedingly cheap in case you were wondering) writing about someone else's stuff unless it was worth while.
But fear not, it's not all going to be me being gushy about some DataGrid. Hell no. In this article, I hope to demonstrate the following concepts:
- Some simple 3D in WPF
- How the Singleton pattern saved the day
- Traversing the visual tree for a templated object
- File system treeview, with lazy loading and icons
- LINQ over objects
- XLINQ usage
- Some slightly sneaky value converters
- Vista style dialogs
There will, of course, be one section on the usage of the Xceed DataGrid, as I think it may help some folk out if they decide to use it in their own applications. I think there is enough here to talk about to keep people interested, at least I hope so.
The demo app
What does the demo app actually do then?
The attached demo application is like a mini Outlook contact keeper; you can manage your friends by adding, removing, and updating them. For each friend, it it possible to assign the following items:
- A name
- An email address
- An image
- A video clip
- A music clip
I guess the only place to start is at the beginning, so I'll just jump in there.
I set out on this one to create a combination of things. I wanted a UI that could show different components by growing and shrinking them to come into view (kind of like the Infragistics WPF showcase Tangerine), but I also wanted to use a 3D display option. These UI modes are mutually exclusive; by that, I mean if you are in grow-shrink mode, you cant use 3D methods, and vice versa. The selection of the current mode is from within an OptionsWindow
, but more on that later. I also wanted the UI to be able to show a grid of data.
I think a nice little screenshot of the flow through the various screens may be in order. I will show bigger screenshots further down the line, as I describe some of the inner workings a bit better.
It can be seen that there is an initial window MaininterfaceWindow
, and from there, you can show three windows (providing you are in grow-shrink UI mode). AddNewFriendControl
is from where you may choose to add an image for your friend by using the AddFriendImageWindow
. From the MaininterfaceWindow
, it is also possible to show the ViewAllUsersControl
which shows all your friends in a data grid, and from there, providing there is a video assigned to your friend, you will be able to show the VideoViewerWindow
to view your friend's associated video. There is also an OptionsWindow
from where you can pick which UI style you want, Grow-Shrink or 3D, and also pick the folder that is used for images by the AddFriendImageWindow
.
That's the basic idea of what the demo app does. What the rest of this article will describe is how some of the more exotic functionality was achieved.
3D
I stumbled across an excellent blog entry by a fellow call Ian G, who had this blog entry about 3D flipping listboxes. Ian simply posted some code, but there was no explanation of how it worked, and it just intrigued me. So I had to rip it to pieces and find out how it worked. The result of this analysis is described below.
OK, so what the heck is all this 3D stuff you're on about Sacha? Well, quite simply, the UI allows a 3D style interaction, as shown below, where to toggle between adding new friends (AddNewFriendControl
) and viewing all friends (ViewAllUsersControl
), the user is able to flip the currently displayed item in a 3D viewport.
The current control is basically rotated around the Y-axis. But how does it achieve this?
Some initial things to note:
- The currently shown control is actually part of a
DataTemplate
. - The
DataTemplate
is actually applied to anItemsControl
(Items3d
in the code). - The
ItemsControl
(Items3d
in the code) only ever contains one item. The contents of which are not important; it's a dummy entry that simply allows the first item within theItemsControl
to be assigned the 3D flippingDataTemplate
. In fact, in the code-behind, you will find the lineitems3d.Items.Add("dont care");
- that's how much we care about the contents of the actual item in theItemsControl
. TheDataTemplate
is where all the real work is done.
That's the basics discussed. What about this DataTemplate
that achieves all this good 3D stuff for us? Well, here it is. Don't worry, I'm going to explain this a bit more thoroughly, as it's fairly complicated.
<DataTemplate x:Key="frontTemplate">
<StackPanel Orientation="Vertical">
<local:AddNewFriendControl x:Name="addFriendsControl3d"
Width="750" Height="500"
SizeChanged="AddNewFriendControl_SizeChanged"/>
<Border Height="20" Background="Yellow"
Width="750" HorizontalAlignment="Center"
CornerRadius="5,5,5,5" BorderBrush="#FFD0601D">
<TextBlock TextAlignment="Center"
FontFamily="Tahoma" FontSize="11"
Text="Click here to see all you friends"/>
</Border>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="backTemplate">
<StackPanel Orientation="Vertical">
<local:ViewAllUsersControl x:Name="viewFriendsControl3d"
Width="750" Height="500"/>
<Border Height="20" Background="Yellow"
Width="750" HorizontalAlignment="Center"
CornerRadius="5,5,5,5" BorderBrush="#FFD0601D">
<TextBlock TextAlignment="Center"
FontFamily="Tahoma" FontSize="11"
Text="Click here to add new friends"/>
</Border>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="flipItemTemplate">
<!-- Note: Camera setup only works when this is square. -->
<!--<Grid Width="800" Height="800"
HorizontalAlignment="Center"
VerticalAlignment="Center">-->
<Grid Margin="0,0,0,0" Width="800"
Height="800" HorizontalAlignment="Center"
VerticalAlignment="Center">
<!-- Provides 3D rotation transition.
Hidden except for when animation is active. -->
<Viewport3D Grid.Column="0" x:Name="vp3D"
Visibility="Hidden" Width="Auto"
Height="Auto" Margin="0,0,0,0" >
<Viewport3D.Camera>
<PerspectiveCamera x:Name="camera"
Position="0,0,0.5"
LookDirection="0,0,-1"
FieldOfView="90" />
</Viewport3D.Camera>
<Viewport3D.Children>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<DirectionalLight Color="#444"
Direction="0,0,-1" />
<AmbientLight Color="#BBB" />
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<!-- Simple flat, square surface -->
<GeometryModel3D.Geometry>
<MeshGeometry3D
TriangleIndices="0,1,2 2,3,0"
TextureCoordinates="0,1 1,1 1,0 0,0"
Positions="-0.5,-0.5,0 0.5,-0.5,0
0.5,0.5,0 -0.5,0.5,0" />
</GeometryModel3D.Geometry>
<!-- Front of shape shows the content of 'frontHost' -->
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush
Visual="{Binding ElementName=frontHost}" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<!-- Back of shape shows the content of 'backHost' -->
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush
Visual="{Binding ElementName=backHost}">
<VisualBrush.RelativeTransform>
<!-- By default, this would come out
backwards because we're on the
back on the shape.
Flip it to make it right. -->
<ScaleTransform ScaleX="-1"
CenterX="0.5" />
</VisualBrush.RelativeTransform>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
<!-- Rotation transform used for transition. -->
<GeometryModel3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
x:Name="rotate"
Axis="0,1,0" Angle="0" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</GeometryModel3D.Transform>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D.Children>
</Viewport3D>
<!-- We use a pair of nested Borders to wrap the content that's going
to go on each side of the rotating model.
The reason is that we need to be able to fade these real bits
of UI in and out as we transition from front to back, but we need
to make sure the VisualBrush in the 3D model doesn't also
get faded out. So the VisualBrush uses the inner Border,
while the fade is applied to the outer one.
-->
<Border x:Name="frontWrapper">
<!-- Note, it's important that this element has visuals
that completely fill the space, as otherwise it messes
with the VisualBrush's size in the 3D model. Setting the background
has that effect, even a transparent one. -->
<Border x:Name="frontHost" Background="Transparent">
<Border.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<!-- Make the Viewport3D visible only
for the duration of the rotation. -->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="vp3D"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{x:Static Visibility.Visible}" />
<DiscreteObjectKeyFrame KeyTime="0:0:1.1"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<!-- Make the background element visible.
(It won't actually appear
until it is faded in right at the end
of the animation.) -->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="backWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:1"
Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<!-- Hide the foreground element.
It will already be invisible
by this time because we fade
it out right at the start
of the animation.
However, until we set its Visibility
to Hidden, it will still be
visible to the mouse... -->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="frontWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.05"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<!-- Fade the front wrapper out.
The Viewport3D is behind us,
so it'll fade into view at this point. The reason
for fading is to avoid a visible step as we
switch from the real UI to the copy
projected onto the 3D model. -->
<DoubleAnimation To="0"
Duration="0:0:0.05"
Storyboard.TargetName="frontWrapper"
Storyboard.TargetProperty="Opacity" />
<!-- Fade the back wrapper in. Once the spin
completes, we fade the real back UI
in over the Viewport3D - using a fade to avoid
a sudden jolt between the slightly fuzzy 3D look
and the real UI. -->
<DoubleAnimation BeginTime="0:0:1.05"
Duration="0:0:0.05" To="1"
Storyboard.TargetName="backWrapper"
Storyboard.TargetProperty="Opacity" />
<!-- 3D animation. Move the camera out slightly
as we spin, so the model fits entirely
within the field of view.
Rotate the model 180 degrees. -->
<Point3DAnimation To="0,0,1.1" From="0,0,0.5"
BeginTime="0:0:0.05" Duration="0:0:0.5"
AutoReverse="True" DecelerationRatio="0.3"
Storyboard.TargetName="camera"
Storyboard.TargetProperty=
"(PerspectiveCamera.Position)" />
<DoubleAnimation From="0" To="180"
AccelerationRatio="0.3"
DecelerationRatio="0.3"
BeginTime="0:0:0.05"
Duration="0:0:1"
Storyboard.TargetName="rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<ContentPresenter Content="{Binding}"
ContentTemplate="{StaticResource frontTemplate}"
VerticalAlignment="Center"/>
</Border>
</Border>
<Border x:Name="backWrapper" Grid.Column="0"
Visibility="Hidden" Opacity="0">
<Border x:Name="backHost" Background="Transparent">
<Border.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="vp3D"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{x:Static Visibility.Visible}" />
<DiscreteObjectKeyFrame KeyTime="0:0:1.1"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="frontWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:1"
Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="backWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.05"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimation To="0" Duration="0:0:0.05"
Storyboard.TargetName="backWrapper"
Storyboard.TargetProperty="Opacity" />
<DoubleAnimation BeginTime="0:0:1.05"
Duration="0:0:0.05"
Storyboard.TargetName="frontWrapper"
Storyboard.TargetProperty="Opacity" />
<Point3DAnimation To="0,0,1.1" From="0,0,0.5"
BeginTime="0:0:0.05" Duration="0:0:0.5"
AutoReverse="True" DecelerationRatio="0.3"
Storyboard.TargetName="camera"
Storyboard.TargetProperty=
"(PerspectiveCamera.Position)" />
<DoubleAnimation From="180" To="360"
AccelerationRatio="0.3" DecelerationRatio="0.3"
BeginTime="0:0:0.05" Duration="0:0:1"
Storyboard.TargetName="rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<ContentPresenter x:Name="backContent" Content="{Binding}"
ContentTemplate="{StaticResource backTemplate}"
VerticalAlignment="Center"/>
</Border>
</Border>
</Grid>
</DataTemplate>
So how does this all work? The basic idea is as follows:
The 3D DataTemplate
The main 3D DataTemplate
is where most of the action happens. And the basic idea is, there is a Viewport3D
which provides a rendering surface for 3-D visual content. The Viewport3D
holds both a Viewport3D.Camera
and the initial GeometryModel3D
, which is the 3D model comprised of a MeshGeometry3D
and a Material
. 3D in WPF also exposes a property for GeometryModel3D.BackMaterial
. This is used within this handsome DataTemplate
. The DataTemplate
actually constructs a fairly simple MeshGeometry3D
as follows:
<!-- Simple flat, square surface -->
<GeometryModel3D.Geometry>
<MeshGeometry3D
TriangleIndices="0,1,2 2,3,0"
TextureCoordinates="0,1 1,1 1,0 0,0"
Positions="-0.5,-0.5,0 0.5,-0.5,0 0.5,0.5,0 -0.5,0.5,0" />
</GeometryModel3D.Geometry>
Perhaps this could do with a little explanation. The Positions
property is the positions in 3D space X, Y, Z planes. We can see that if this were mapped out, we would get something like:
And the TriangleIndices
property is the indices of the triangles that make up the GeometryModel3D.Geometry
, in this case a simple square, which is made from two separate triangles. This is how 3D works. Let's see these two triangles:
That's how we get the initial shape, basically a square that will hold some content. How about the content? Where does that come from?
Recall, I said that this 3D Datatemplate
actually allows us to rotate around the Y-axis, so there should be a front and back. Which indeed there is:
The front section is made up as follows:
<!-- Simple flat, square surface -->
<GeometryModel3D.Geometry>
<MeshGeometry3D
TriangleIndices="0,1,2 2,3,0"
TextureCoordinates="0,1 1,1 1,0 0,0"
Positions="-0.5,-0.5,0 0.5,-0.5,0 0.5,0.5,0 -0.5,0.5,0" />
</GeometryModel3D.Geometry>
.......
.......
.......
.......
<!-- We use a pair of nested Borders to wrap the content that's going to go on
each side of the rotating model.
The reason is that we need to be able to fade these real bits of UI in and out
as we transition from front to back, but we need to make sure the VisualBrush
in the 3D model doesn't also get faded out. So the VisualBrush uses the inner
Border, while the fade is applied to the outer one.
-->
<Border x:Name="frontWrapper">
<!-- Note, it's important that this element
has visuals that completely fill the space, as
otherwise it messes with the VisualBrush's size
in the 3D model. Setting the background
has that effect, even a transparent one. -->
<Border x:Name="frontHost" Background="Transparent">
<Border.Triggers>
<EventTrigger RoutedEvent="Grid.MouseDown">
<BeginStoryboard>
<Storyboard>
<!-- Make the Viewport3D visible only
for the duration of the rotation. -->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="vp3D"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0"
Value="{x:Static Visibility.Visible}" />
<DiscreteObjectKeyFrame KeyTime="0:0:1.1"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<!-- Make the background element visible.
(It won't actually
appear until it is faded in right
at the end of the animation.) -->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="backWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:1"
Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<!-- Hide the foreground element.
It will already
be invisible by this time because
we fade it out right
at the start of the animation.
However, until we set its Visibility
to Hidden, it will still be visible
to the mouse... -->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="frontWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:0.05"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<!-- Fade the front wrapper out. The Viewport3D
is behind us, so it'll fade into view at this point.
The reason for fading is to avoid a visible step as we
switch from the real UI
to the copy projected onto the 3D model. -->
<DoubleAnimation To="0"
Duration="0:0:0.05"
Storyboard.TargetName="frontWrapper"
Storyboard.TargetProperty="Opacity" />
<!-- Fade the back wrapper in. Once the spin completes,
we fade the real back UI
in over the Viewport3D - using a fade
to avoid a sudden jolt between the slightly
fuzzy 3D look and the real UI. -->
<DoubleAnimation BeginTime="0:0:1.05"
Duration="0:0:0.05" To="1"
Storyboard.TargetName="backWrapper"
Storyboard.TargetProperty="Opacity" />
<!-- 3D animation. Move the camera out slightly as we spin,
so the model fits entirely within the field of view.
Rotate the model 180 degrees. -->
<Point3DAnimation To="0,0,1.1" From="0,0,0.5"
BeginTime="0:0:0.05" Duration="0:0:0.5"
AutoReverse="True" DecelerationRatio="0.3"
Storyboard.TargetName="camera"
Storyboard.TargetProperty=
"(PerspectiveCamera.Position)" />
<DoubleAnimation From="0" To="180"
AccelerationRatio="0.3" DecelerationRatio="0.3"
BeginTime="0:0:0.05" Duration="0:0:1"
Storyboard.TargetName="rotate"
Storyboard.TargetProperty="Angle" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<ContentPresenter Content="{Binding}"
ContentTemplate="{StaticResource frontTemplate}"
VerticalAlignment="Center"/>
</Border>
</Border>
Where the GeometryModel3D.Material
uses a VisualBrush
that is bound to an existing Element within the main DataTemplate
. This can be seen on the line:
<VisualBrush Visual="{Binding ElementName=frontHost}" />
Where the element GeometryModel3D.Material
is being bound to its frontHost
. If we then dig a little deeper and look at the actual element frontHost
(shown above), we can see that its a Border
that holds various animations targeting various elements such as frontWrapper
, backWrapper
, camera
, and rotate
. To perform these animations, several different types of animations have been used; there are ObjectAnimationUsingKeyFrames
, DoubleAnimation
, Point3DAnimation
, all of which target different properties within the main DataTemplate
that allow the 3D model to be rotated. The basic idea with the various animations is that when the current front control shown is clicked, the current front control will gradually be rotated (around the Y-axis) and changed to invisible, and at the end of the animation cycle, the other (not current) control will be shown. If you are more curious about this, just examine the various animations, you will see it, it's fairly OK actually.
The last thing of interest within the frontHost
element is that there is a ContentPresenter
which targets yet another DataTemplate
for its actual ControlTemplate
. Let's see this:
<ContentPresenter
Content="{Binding}" ContentTemplate="{StaticResource frontTemplate}"
VerticalAlignment="Center"/>
And, if we look at the frontTemplate
DataTemplate
(which was right at the top of the main DataTempate
full source code), we can see that it actually uses an instance of a AddNewFriendControl
which is the front control that you see.
<DataTemplate x:Key="frontTemplate">
<StackPanel Orientation="Vertical">
<local:AddNewFriendControl x:Name="addFriendsControl3d"
Width="750" Height="500"
SizeChanged="AddNewFriendControl_SizeChanged"/>
<Border Height="20" Background="Yellow" Width="750"
HorizontalAlignment="Center"
CornerRadius="5,5,5,5" BorderBrush="#FFD0601D">
<TextBlock TextAlignment="Center"
FontFamily="Tahoma" FontSize="11"
Text="Click here to see all you friends"/>
</Border>
</StackPanel>
</DataTemplate>
This is how the AddNewFriendControl
ends up being within the 3D Viewport. The same principle is applied to the BackMaterial
where a separate binding is used on a VisualBrush
to the backHost
element. Which in turn uses the backTemplate
DataTemplate
for its own ContentPresenter
.
The back loads the ViewAllUsersControl
:
And that's how the 3D DataTemplate
works. Neat, huh?
How the Singleton pattern saved the day
OK, so we have gone through some of the 3D stuff, which is really just talking about how the UI works in one mode. But what does the UI actually do? Well, it's pretty straightforward really; it does the following:
- There is a UserControl:
AddNewFriendControl
that allows new friends to be added. - There is a UserControl:
ViewAllUsersControl
which shows all the friends in the Xceed WPF DataGrid.
That's it really; of course, there are a few helper screens along the way. But in essence, that's it.
So, why am I talking about this in a section entitled Singleton Pattern yada yada yada? Well, it's like this. In the application, there is a concept of different types of display mode. You can either be in grow and shrink mode, in which case, a Grid
(gridHolder
) that is normally hidden is moved in to be a child element of the main display Grid
(mainGrid
), and the 3D ItemsControl
(items3d
) is removed as a child from the main display Grid
(mainGrid
), and vice versa. And as we now know, the 3D DataTemplate
that we just discussed above also contains a copy of both a AddNewFriendControl
and a ViewAllUsersControl
. So surely, the content of these two copies of the controls needs be kept in synch somehow, as the user could potentially, half way through an operation, decide to change the UI mode. So that's where we need the Singleton pattern. It's quite a life saver actually. There is a class FriendContent
which provides the Singleton content for the AddNewFriendControl
. This is a simple class that simply stores values for all the possible entries within the AddNewFriendControl
. If we have a look at the AddNewFriendControl
, it may be clearer to see what properties are catered for within the FriendContent
class.
It can be seen that there are properties available for five items:
- Name
- Image URL
- Video URL
- Music URL
So, it is no surprise then that the FriendContent
class provides these same five properties that may be used by both AddNewFriendControl
controls, the one shown in grow-shrink mode and the one used in the 3D DataTemplate
discussed earlier. You see the grow-shrink copy of the AddNewFriendControl
controls exist in the Windows Logical Tree, as it's a normal child to a Grid
control, so could be accessed in the code-behind. But the copy of the AddNewFriendControl
control that is part of the 3D DataTemplate
is a little trickier, as one can not simply refer to this by name, as it's part of a control's DataTemplate
and so is not part of the overall logical tree.
Anyway, all that aside, the FriendContent
class looks like this, which I think is self-explanatory. Oh, one thing to note is that this class is using the new C# syntax for automatic properties. Josh Smith raised an interesting issue about this style of creating properties in this thread, which I urge you all to read.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyFriends
{
public class FriendContent
{
public string FriendName { get; set; }
public string FriendEmail { get; set; }
public string PhotoUrl { get; set; }
public string VideoUrl { get; set; }
public string MusicUrl { get; set; }
private static FriendContent instance;
private FriendContent()
{
}
public void Reset()
{
FriendName = string.Empty;
FriendEmail = string.Empty;
PhotoUrl = string.Empty;
VideoUrl = string.Empty;
MusicUrl = string.Empty;
}
public static FriendContent Instance()
{
if (instance == null)
{
instance = new FriendContent();
}
return instance;
}
}
}
So the idea is that, we use this Singleton class to tell the AddNewFriendControl
to update their content based on the single set of values within the FriendContent
class.
Traversing the visual tree for a templated object
As I just stated, those UI elements that belong to the logical tree of the window, it is no problem to simply get a reference to the correct item and change its properties directly. However, I also stated that one copy of this is actually part of a 3D DataTemplate
, which is applied to the single (Dummy) item in an ItemsControl
. So getting to them is a little bit trickier. Luckily, we only have to care about updating these two controls that are part of the 3D DataTemplate
when the display mode is changed to 3D. So, I looked around for an event that occurs whenever the user changes to 3D mode. As luck would have it, whenever the display mode is changed to 3D, I noticed that the SizeChanged
event of the AddNewFriendControl
got fired. Excellent, so we can use that to update the content of both of the controls within the 3D DataTemplate
.
Let's just take a minute. What are we trying to achieve? We are trying to get the two controls that are part of the 3D DataTemplate
to update to the latest content that will have been filled in on the grow-shrink copies of the AddNewFriendControl
and the ViewAllUsersControl
controls. But in order to do this, we are going to need a reference to both of these controls in the code-behind file. That sounds easy, right? Wrong, it's a bit of a trick. Let's see. I should say it took me quite a while to come up with this code, so read it carefully dear reader.
void AddNewFriendControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
//obtaining the AddNewFriendControl is easy just
//use the sender and get it to ReInitialise
//which will fetch the latest content from the FriendContent singleton
addfriendsControl3D = sender as AddNewFriendControl;
addfriendsControl3D.ReInitialise();
//Obtaining an instance of the ViewAllUsersControl
//is a little tricker, as we need to
//find it in the DataTemplate itself,
//which means we need to walk its VisualTree
DependencyObject item = null;
//there will be only 1 item, we are simply using
//the item as a sneaky way to apply
//out custom 3d tempplate
foreach (object dataitem in items3d.Items)
{
//get the UIElement for the ItemsControl item
item = items3d.ItemContainerGenerator.ContainerFromItem(dataitem);
int count = VisualTreeHelper.GetChildrenCount(item);
for (int i = 0; i < count; i++)
{
DependencyObject itemFetched = VisualTreeHelper.GetChild(item, i);
//look for a grid, which is the one
//we need to allow use to find the relevant
//ContentPresenter that hosts our ViewAllUsersControl
if (itemFetched is Grid)
{
//do back content, and make sure all properties are copies across
ContentPresenter cp = (itemFetched as Grid).FindName(
"backContent") as ContentPresenter;
DataTemplate myDataTemplate = cp.ContentTemplate;
ViewAllUsersControl viewUsers = (ViewAllUsersControl)
myDataTemplate.FindName("viewFriendsControl3d", cp);
viewUsers.Height = (sender as AddNewFriendControl).Height;
viewUsers.DataBind();
return;
}
}
}
}
I think for this section of code to make sense, I'm going to have to show a portion of the 3D DataTemplate
again.
It can be seen that the first thing to find is the Grid
, and then try and get the ContentPresenter
for the backContent
, and then from there, it's just a case of grabbing the ContentPresenter
s applied ContentTemplate
, and bingo, there you have it: a reference to a control in a template. From there, we can call its methods and set its properties. Easy, right?
So now that we have a reference to these two controls within the 3D DataTemplate
, what do we do with them to get them to update their content? Well, that part is actually easy. In the case of the AddNewFriendControl
, we simply call the ReInitialise()
method, which is as follows:
public void ReInitialise()
{
friendContent = FriendContent.Instance();
initialising = true;
txtFriendName.Text = friendContent.FriendName;
txtEmail.Text = friendContent.FriendEmail;
//photo
if (friendContent.PhotoUrl != null)
if (!friendContent.PhotoUrl.Equals(string.Empty))
photoSrc.Source =
new BitmapImage(new Uri(friendContent.PhotoUrl));
//video
if (friendContent.VideoUrl != null)
if (!friendContent.VideoUrl.Equals(string.Empty))
videoSrc.Source = new Uri(friendContent.VideoUrl);
//music
if (friendContent.MusicUrl != null)
if (!friendContent.MusicUrl.Equals(string.Empty))
musicSrc.Source = new Uri(friendContent.MusicUrl);
initialising = false;
}
Which, as you can see, uses the FriendContent
singleton we talked about earlier. In the case of the ViewAllUsersControl
control, we simply call the DataBind()
method which will cause the Xceed WPF data grid to rebind its contents. This is shown below, and uses a second singleton FriendsList
that will be discussed later:
public void DataBind()
{
dgFriends.ItemsSource = FriendsList.Instance();
}
File system treeview, with lazy loading and icons
The OptionsWindow
allows users to change between display styles, but it also shows a lazy loaded treeview that shows the directory structure of the host computer. This treeview can be used to pick the source directory that is used by the AddFriendImageWindow
.
I have been working on this article for a while, so I split the treeview implementation into a separate article, which is described here, which was published some time ago. Josh Smith, being Josh (which is cool) had a suggestion with this article, and posted an alternative approach which is published here, and then rather amusingly, Karl Shiflett also had an alternative approach which he published here. So there you have it, you are now spoilt for choice. I've kept my implementation the same as I originally published it, though if I was going to change, I would probably go with Josh's suggestion as it makes most sense to me.
LINQ over objects
Once you have picked a directory within the OptionsWindow
that has images in it, there is an additional window that is accessible from a button underneath the user's image on the AddNewFriendControl
. When clicked, this button shows the AddFriendImageWindow
, which is as shown below:
I make use of Paul Tallet's excellent Fisheye panel on this page. But I also allow the user to scroll through the image folder selected on the OptionsWindow
using LINQ as follows:
private void GetImages(int pageIndex)
{
try
{
var imgs = (from fi in Files select fi.FullName).
IsImageFile().Skip(pageIndex * NumOfImageToFetch).
Take(NumOfImageToFetch);
//NOTE : We could have also used the version of the IsImageFile()
//custom LINQ string extension method
//that expects a predicate, something like
//IsImageFile(f => f.StartsWith("png") ||
// f.StartsWith("jpg").Skip
fishPanel.Children.Clear();
foreach (string filename in imgs)
{
if (UsingReflectiveImages.Value)
{
StoredImageControl si = new StoredImageControl
{
OriginalFileUrl = filename,
Margin = new Thickness(5)
};
si.MouseDown +=
new System.Windows.Input.MouseButtonEventHandler(si_MouseDown);
fishPanel.Children.Add(si);
}
else
{
StoredImage si = new StoredImage
{
Source = new BitmapImage(new Uri(filename)),
Width = 100,
OriginalFileUrl = filename,
Margin = new Thickness(5)
};
si.MouseDown += new
System.Windows.Input.MouseButtonEventHandler(si_MouseDown);
fishPanel.Children.Add(si);
}
}
btnPrev.IsEnabled = pageIndex > 0;
btnNext.IsEnabled =
(Files.Length - (++pageIndex * NumOfImageToFetch)) >= 10;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The more eagle eyed among you may notice that there is a slightly curious bit of syntax, IsImageFile()
used in the LINQ query. Mmm, how can that be? Well, the nice folk at Microsoft have now allowed us to create our own LINQ extensions, and that is exactly what this IsImageFile()
thing is. Let us see this example:
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Data.Linq;
using System.Xml.Linq;
using System.Xml;
namespace MyFriends
{
public static class CustomStringExtensions
{
public static IEnumerable<string> IsImageFile(
this IEnumerable<string> files,
Predicate<string> isMatch)
{
foreach (string file in files)
{
if (isMatch(file))
yield return file;
}
}
public static IEnumerable<string>
IsImageFile(this IEnumerable<string> files)
{
foreach (string file in files)
{
if (file.Contains(".jpg") ||
file.Contains(".png") ||
file.Contains(".bmp"))
yield return file;
}
}
}
}
It can be seen that there are two methods for IsImageFile
that both return IEnumerable<T>
(string in this case), which is what LINQ extensions require an extension to provide. Another strange thing is that there is a this
just shoved in the method signature; that bit of syntax allows the result of the query so far to be used in the new extension (this one effectively) being applied. The only other thing to note is that the type after the this
keyword must match the current query result. So in the previous example, we are doing:
(from fi in Files select fi.FullName)
Which does indeed yield IEnumerable<string>
so this extension IsImageFile
may be used. Such as:
var imgs = (from fi in Files select fi.FullName).
IsImageFile().Skip(pageIndex * NumOfImageToFetch).
Take(NumOfImageToFetch);
XLINQ usage
The persistence of the friends that have been added to the demo app is done 100% using XLINQ. It works as follows:
- When the Save Friend button on the
AddNewFriendControl
is clicked, a check is done to see if there is an XML in existence. If there isn't, a new XML file is created using XLINQ. A newFriend
object is added to an internal collection of objects used by the data grid. - When the Save Friend button is clicked again, and there is no in memory held
Friend
objects, append to the XML file and add a newFriend
object to the internal collection of objects used by the data grid. However, if there is no XML, just add a newFriend
object to the internal collection of objects used by the data grid. - On application closure, store all the in memory held
Friend
objects to an XML file that will overwrite the existing XML file (if it exists).
That is the basic idea; write the XML file initially, bind the result of the file to an internal collection which the grid uses, and then maintain the internally held collection, and on exit, update the XML file on disk. Likewise, on load, read the XML file from disk into the memory held collection:
So shall we see some code? Well, the code for the AddNewFriendControl
Save Friend button is as shown below:
private void btnSave_Click(object sender, RoutedEventArgs e)
{
string xmlFilename =
(string)Application.Current.Properties["SavedDetailsFileName"];
string fullXmlPath =
Path.Combine(Environment.CurrentDirectory, xmlFilename);
bool allRequiredFieldsFilledIn = true;
allRequiredFieldsFilledIn = IsEntryValid(txtFriendName) &&
IsEntryValid(txtEmail);
allRequiredFieldsFilledIn = IsEmailValid(txtEmail.Text);
if (allRequiredFieldsFilledIn)
{
if (File.Exists(fullXmlPath))
{
try
{
//if there is currently no XML and no Friends in
//memory, append to file. This should never happen
if (FriendsList.Instance().Count == 0)
{
Friend friend = new Friend
{
ID = Guid.NewGuid(),
Name = friendContent.FriendName,
Email = friendContent.FriendEmail,
PhotoUrl = friendContent.PhotoUrl,
VideoUrl = friendContent.VideoUrl,
MusicUrl = friendContent.MusicUrl
};
XMLFileOperations.AppendToFile(fullXmlPath, friend);
FriendsList.Instance().Add(friend);
RaiseEvent(new RoutedEventArgs(FriendAddedEvent));
friendContent.Reset();
this.Reset();
MessageBox.Show("Sucessfully saved friend");
}
//otherwise simply update the singleton in memory
//collection of friends, which will be written
//to disk at closure of the application
else
{
FriendsList.Instance().Add(new Friend
{
ID = Guid.NewGuid(),
Name = friendContent.FriendName,
Email = friendContent.FriendEmail,
PhotoUrl = friendContent.PhotoUrl,
VideoUrl = friendContent.VideoUrl,
MusicUrl = friendContent.MusicUrl
});
RaiseEvent(new RoutedEventArgs(FriendAddedEvent));
friendContent.Reset();
this.Reset();
MessageBox.Show("Sucessfully saved friend");
}
}
catch
{
MessageBox.Show("Error updating friends details");
}
}
else
{
try
{
Friend friend = new Friend
{
ID = Guid.NewGuid(),
Name = friendContent.FriendName,
Email = friendContent.FriendEmail,
PhotoUrl = friendContent.PhotoUrl,
VideoUrl = friendContent.VideoUrl,
MusicUrl = friendContent.MusicUrl
};
XMLFileOperations.CreateInitialFile(fullXmlPath, friend);
FriendsList.Instance().Add(friend);
RaiseEvent(new RoutedEventArgs(FriendAddedEvent));
friendContent.Reset();
this.Reset();
MessageBox.Show("Sucessfully saved friend");
}
catch(Exception ex)
{
MessageBox.Show("Error saving friends details");
}
}
}
else
{
MessageBox.Show("You need to either fill in one of the fields, " +
"or correct it", "Error", MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
And recall, earlier I mentioned that there was a second singleton that was used by the ViewAllUsersControl
to allow it to maintain the correct data to show in the data grid. Well, that's the FriendsList
singleton shown below:
using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Text;
using System.Windows;
namespace MyFriends
{
public class FriendsList : ObservableCollection<Friend>
{
private static FriendsList instance;
private FriendsList()
{
try
{
XMLFileOperations.XmlFilename =
(string)Application.Current.Properties["SavedDetailsFileName"];
List<Friend> theList = XMLFileOperations.GetFriends();
foreach (Friend friend in theList)
{
this.Add(friend);
}
}
catch { }
}
public static FriendsList Instance()
{
if (instance == null)
{
instance = new FriendsList();
}
return instance;
}
}
}
But wait, this also calls yet another class to get its data, so the story isn't finished yet. Let's follow this path. There is another class called XMLFileOperations
. Let's see the method in that class.
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Data.Linq;
using System.Xml.Linq;
using System.Xml;
using System.IO;
namespace MyFriends
{
public class XMLFileOperations
{
public static string XmlFilename { get; set; }
public static List<Friend> GetFriends()
{
string fullXmlPath = System.IO.Path.Combine(Environment.CurrentDirectory,
XMLFileOperations.XmlFilename);
var xmlFriendResults =
from friend in StreamElements(fullXmlPath, "Friend")
select new Friend
{
ID = new Guid(friend.Element("ID").Value),
Name = friend.Element("name").SafeValue(),
Email = friend.Element("email").SafeValue(),
PhotoUrl = friend.Element("photo").SafeValue(),
VideoUrl = friend.Element("video").SafeValue(),
MusicUrl = friend.Element("music").SafeValue()
};
return xmlFriendResults.ToList();
}
//*************************************************************
// NOTE : THIS IS HOW YOU WOULD LOAD THE DOCUMENT IN ONE GO
// BUT IF YOU HAVE A LARGE XML DOCUMENT THE LOAD(..)
// METHOD MIGHT BE A BOTTLE NECK
//*************************************************************
//public static IEnumerable<XElement> GetFriendItems(string uri)
//{
// var xmlDoc = XDocument.Load(uri);
// var xmlElement =
// xmlDoc.Root.Element("MyFriends").Elements("Friend");
// foreach (var xmlElement in xmlElement)
// yield return xmlElement;
//}
public static void CreateInitialFile(string fullXmlPath, Friend friend)
{
XElement friendsXmlDocument =
new XElement("MyFriends",
new XElement("Friend",
new XElement("ID", friend.ID),
new XElement("name", friend.Name),
new XElement("email", friend.Email),
new XElement("photo", friend.PhotoUrl),
new XElement("video", friend.VideoUrl),
new XElement("music", friend.MusicUrl))
);
friendsXmlDocument.Save(fullXmlPath);
}
public static void AppendToFile(string fullXmlPath, Friend friend)
{
XElement friendsXmlDocument = XElement.Load(fullXmlPath);
friendsXmlDocument.Add(new XElement("Friend",
new XElement("ID", friend.ID),
new XElement("name", friend.Name),
new XElement("email", friend.Email),
new XElement("photo", friend.PhotoUrl),
new XElement("video", friend.VideoUrl),
new XElement("music", friend.MusicUrl))
);
friendsXmlDocument.Save(fullXmlPath);
}
public static IEnumerable<XElement> StreamElements(string uri, string name)
{
using (XmlReader reader = XmlReader.Create(uri))
{
reader.MoveToContent();
while (reader.Read())
{
if ((reader.NodeType == XmlNodeType.Element) &&
(reader.Name == name))
{
XElement element = (XElement)XElement.ReadFrom(reader);
yield return element;
}
}
reader.Close();
}
}
public static void SaveOnExit()
{
string xmlFilename = (string)
System.Windows.Application.Current.Properties["SavedDetailsFileName"];
string fullXmlPath = Path.Combine((string)
System.Windows.Application.Current.Properties["SaveFolder"],
xmlFilename);
XDocument document = new XDocument(
new XElement("MyFriends", getExistingElements()));
document.Save(fullXmlPath);
}
private static List<XElement> getExistingElements()
{
List<XElement> elements = new List<XElement>();
foreach (Friend friend in FriendsList.Instance())
{
elements.Add(new XElement("Friend",
new XElement("ID", friend.ID),
new XElement("name", friend.Name),
new XElement("email", friend.Email),
new XElement("photo", friend.PhotoUrl),
new XElement("video", friend.VideoUrl),
new XElement("music", friend.MusicUrl)));
}
return elements;
}
}
}
Most of this little lot is standard XLINQ, with the exception of one little method; the StreamElements()
is special. And why is it special? Well, it allows us to return a single IEnumerable<XElement>
to a standard XLINQ query one element at a time. Which will help for large XML files where loading time may be a consideration. It uses the fairly new C# yield
keyword.
Also of note in this little lot is yet another custom LINQ extension, the SafeValue
XLINQ extension that targets XElement
objects. This XLINQ extension is shown below:
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Data.Linq;
using System.Xml.Linq;
using System.Xml;
namespace MyFriends
{
public static class CustomXElementExtensions
{
public static string SafeValue(this XElement input)
{
return (input == null) ? string.Empty : (string)input.Value;
}
}
}
So with this in place, we are able to write things like:
friend.Element("name").SafeValue()
And our nice little SafeValue()
XLINQ extension ensures we will get a nice value back instead of null
.
I hope you are seeing the value that LINQ will bring to us as developers. I think, used carefully, it's well cool and powerful.
Getting gushy about the Xceed data grid
Like I said right at the beginning of this article, I would not normally include a third party product unless I thought it was of some use to me or you for that matter. And I have to say, the free Xceed datagrid for WPF is just awesome. You can basically do the following:
- Create cell templates
- Create cell edit templates
- Create cell validators
- Create different views
In fact, you can totally restyle it, if that's your bag. Me, I just wanted to try it out for size. Basically, I've tried all the things above, so I'll talk about each of them in turn.
The first thing to note is that the Xceed data grid is hosted within the ViewAllUsersControl
control. As such, all the relevant mark up is within the ViewAllUsersControl.xaml file. Another thing to note is that the Xceed data grid is bound to the results of the FriendsList
object, which is an ObservableCollection<Friend>
type of object. But we still haven't seen the Friend
object, have we? Probably best to have a quick look at that, so we can see where the grid bindings are coming from.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyFriends
{
public class Friend
{
public Guid ID { get; set; }
public string Email { get; set; }
public string Name { get; set; }
public string PhotoUrl { get; set; }
public string VideoUrl { get; set; }
public string MusicUrl { get; set; }
}
}
The bound grid looks something like this:
That's simple, isn't it? Anyway, let's crack on.
Create cell templates
This is a synch; all that you have to do is create a column and define a template for it. An example of this is shown below for the ImageUrl
bound column:
<!-- Photo Column-->
<xcdg:Column FieldName="PhotoUrl" VisiblePosition="3" Visible="True">
<!-- Content Non-Edit mode -->
<xcdg:Column.CellContentTemplate>
<DataTemplate>
<StackPanel Margin="5,5,5,5"
VerticalAlignment="Center"
HorizontalAlignment="Left">
<Border BorderBrush="White" BorderThickness="2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image x:Name="img"
Source="{Binding}" Stretch="Fill"
Width="46" Height="46">
<Image.ToolTip>
<Border BorderBrush="White"
BorderThickness="6"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image Source="{Binding}"
Width="150" Height="150"
Stretch="Fill"
x:Name="imgTool"></Image>
</Border>
</Image.ToolTip>
</Image>
</Border>
<Border Width="50" Height="50"
BorderBrush="White" BorderThickness="2"
HorizontalAlignment="Center" >
<Border.Background>
<VisualBrush Visual="{Binding ElementName=img}">
<VisualBrush.Transform>
<ScaleTransform ScaleX="1" ScaleY="-1"
CenterX="50"
CenterY="25"></ScaleTransform>
</VisualBrush.Transform>
</VisualBrush>
</Border.Background>
<Border.OpacityMask>
<LinearGradientBrush
StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0"
Color="Black"></GradientStop>
<GradientStop Offset="0.6"
Color="Transparent"></GradientStop>
</LinearGradientBrush>
</Border.OpacityMask>
</Border>
</StackPanel>
</DataTemplate>
</xcdg:Column.CellContentTemplate>
As the Xceed datagrid allows us to create our own Template
s, we can simply create whatever content we want for a particular cell, and that's what I've done. In this example, we have an image being shown in a cell using a reflection. And this looks like the cell below:
Create cell editor templates
This is actually pretty easy too, as Xceed has made that pretty easy by just creating another type of Template
, an EditTemplate
. Let's see one of these:
<xcdg:Column.CellEditor>
<xcdg:CellEditor>
<xcdg:CellEditor.EditTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,0" Orientation="Vertical"
Background="{StaticResource blackLinearBrush}"
VerticalAlignment="Center" HorizontalAlignment="Left">
<Border BorderBrush="White" BorderThickness="2"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image x:Name="imgNew"
Source="{xcdgWeb:CellEditorBinding}"
Stretch="Fill"
Width="46" Height="46">
<Image.ToolTip>
<Border BorderBrush="White"
BorderThickness="6"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image Source="{Binding}" Width="150"
Height="150"
Stretch="Fill"></Image>
</Border>
</Image.ToolTip>
</Image>
</Border>
<Button x:Name="btnAssignNewImage" Content="`"
Template="{DynamicResource GlassButton}"
FontFamily="Webdings" FontSize="15"
FontWeight="Normal" Foreground="#FFFFFFFF"
ToolTip="Assign New Image" Width="50"
Height="25" Margin="0,5,0,0"
HorizontalAlignment="Center"
Click="btnAssignNewImage_Click"/>
</StackPanel>
</DataTemplate>
</xcdg:CellEditor.EditTemplate>
</xcdg:CellEditor>
</xcdg:Column.CellEditor>
Of course, in this case, as we are actually applying a new value, we need some code-behind functionality to do the edit, such as:
private void btnAssignNewImage_Click(object sender, RoutedEventArgs e)
{
Point topleft = this.PointToScreen(new Point(0, 0));
DisplayStyle newDisplayStle =
(DisplayStyle)Application.Current.Properties["SelectedDisplayStyle"];
double heightOffset = newDisplayStle == DisplayStyle.ThreeDimension ? 20 : 0;
AddFriendImageWindow addImageWindow = new AddFriendImageWindow();
(addImageWindow as Window).Height = this.Height + heightOffset;
(addImageWindow as Window).Width = this.Width;
(addImageWindow as Window).Left = topleft.X;
(addImageWindow as Window).Top = topleft.Y;
addImageWindow.ShowDialog();
if (!string.IsNullOrEmpty(addImageWindow.SelectedImagePath))
{
StackPanel panel =
VisualTreeHelper.GetParent(sender as DependencyObject) as StackPanel;
Image image = panel.FindName("imgNew") as Image;
if (image != null)
{
image.Source =
new BitmapImage(new Uri(addImageWindow.SelectedImagePath));
}
}
}
Still fairly OK, me thinks. Not so painful, is it?
Create cell validators
Another thing I personally like with this grid is that it is possible to plug in a validator into certain cells. Let's see an example of this in action.
So we define a cell to have validation like this:
<xcdg:Column FieldName="Email" VisiblePosition="2"
CellErrorStyle="{StaticResource cell_error}" Visible="True">
<xcdg:Column.CellValidationRules>
<local:EmailValidationRule/>
</xcdg:Column.CellValidationRules>
</xcdg:Column>
And then we have a validator like:
using System;
using System.Collections.Generic;
using System.Text;
using Xceed.Wpf.DataGrid;
using Xceed.Wpf.DataGrid.ValidationRules;
using System.Windows.Controls;
using System.Globalization;
using System.Text.RegularExpressions;
namespace MyFriends
{
public class EmailValidationRule : CellValidationRule
{
public override ValidationResult
Validate(object value, CultureInfo cultureInfo,
CellValidationContext cellValidationContext)
{
string pattern =
@"^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$";
Regex regEx=new Regex(pattern);
if (!regEx.IsMatch((string)value))
{
return new ValidationResult(false,
"You entered an invalid email");
}
return new ValidationResult(true, null);
}
}
}
And then at runtime, we have a validator in action:
Create a different view
The Xceed grid supports several different views straight out of the box, such as a table and card. I have provided two buttons to toggle between these views. The switching between these views is easily achieved as follows:
private void btnTableView_Click(object sender, RoutedEventArgs e)
{
TableView tv = new TableView();
tv.Theme = new LunaMetallicTheme();
dgFriends.View = tv;
}
private void btnCardView_Click(object sender, RoutedEventArgs e)
{
CardView cv = new CardView();
cv.Theme = new LunaMetallicTheme();
dgFriends.View = cv;
}
And to see what the data grid looks like in card view:
All in all, I am very, very impressed by the Xceed grid, and the fact that it's free should not be overlooked. I'll be using it on a real project if my requirements need some sort of tabular data.
Some slightly sneaky value converters
As part of this application, I wanted to be able to hide a certain element based on whether another element's source was empty or not. To this end, I crafted a ValueConverter
that does this trick, which is as shown below. This is used within the Xceed data grid cell templates to ensure that for both the video and music cells, the play/stop/view buttons are only shown if the associated MediaElement
's source is not empty.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace MyFriends
{
[ValueConversion(typeof(Uri), typeof(Visibility))]
public class SourceToVisibilityConverter : IValueConverter
{
#region Instance Fields
public static SourceToVisibilityConverter Instance =
new SourceToVisibilityConverter();
#endregion
#region IValueConverter implementation
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
try
{
return (value as Uri).AbsolutePath.Equals(string.Empty) ?
Visibility.Collapsed : Visibility.Visible;
}
catch
{
return Visibility.Collapsed;
}
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotSupportedException("Cannot convert back");
}
#endregion
}
}
So we can use this converter as follows in the XAML:
Visibility="{Binding Path=Source, ElementName=videoSrc,
Mode=Default,
Converter={x:Static local:SourceToVisibilityConverter.Instance}}"
Vista style dialogs
One last thing, then were are done... I happened to notice during this application that some of the common dialogs such as Open/Save were not displaying as my own Vista dialogs were, so I hunted around and found some code in the SDK examples that did the trick. This is shown below:
using System;
using System.Windows;
using System.Windows.Interop;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace MyFriends
{
/// <summary>
/// One item in the common dialog filter.
/// </summary>
public class FilterEntry
{
private string display;
private string extention;
public string Display
{
get { return display; }
}
public string Extention
{
get { return extention; }
}
public FilterEntry(string display, string extension)
{
this.display = display;
this.extention = extension;
}
}
/// <summary>
/// Displays the common Open and SaveAs dialogs using the Vista-style dialogs.
/// </summary>
class CommonDialog
{
#region fields
// Structure used when displaying Open and SaveAs dialogs.
private OpenFileName ofn = new OpenFileName();
// List of filters to display in the dialog.
private List<FilterEntry> filter = new List<FilterEntry>();
#endregion
#region properties
public List<FilterEntry> Filter
{
get { return filter; }
}
public string Title
{
set { ofn.title = value; }
}
public string InitialDirectory
{
set { ofn.initialDir = value; }
}
public string DefaultExtension
{
set { ofn.defExt = value; }
}
public string FileName
{
get { return ofn.file; }
}
#endregion
#region pinvoke details
private enum OpenFileNameFlags
{
OFN_READONLY = 0x00000001,
OFN_OVERWRITEPROMPT = 0x00000002,
OFN_HIDEREADONLY = 0x00000004,
OFN_NOCHANGEDIR = 0x00000008,
OFN_SHOWHELP = 0x00000010,
OFN_ENABLEHOOK = 0x00000020,
OFN_ENABLETEMPLATE = 0x00000040,
OFN_ENABLETEMPLATEHANDLE = 0x00000080,
OFN_NOVALIDATE = 0x00000100,
OFN_ALLOWMULTISELECT = 0x00000200,
OFN_EXTENSIONDIFFERENT = 0x00000400,
OFN_PATHMUSTEXIST = 0x00000800,
OFN_FILEMUSTEXIST = 0x00001000,
OFN_CREATEPROMPT = 0x00002000,
OFN_SHAREAWARE = 0x00004000,
OFN_NOREADONLYRETURN = 0x00008000,
OFN_NOTESTFILECREATE = 0x00010000,
OFN_NONETWORKBUTTON = 0x00020000,
OFN_NOLONGNAMES = 0x00040000,
OFN_EXPLORER = 0x00080000,
OFN_NODEREFERENCELINKS = 0x00100000,
OFN_LONGNAMES = 0x00200000,
OFN_ENABLEINCLUDENOTIFY = 0x00400000,
OFN_ENABLESIZING = 0x00800000,
OFN_DONTADDTORECENT = 0x02000000,
OFN_FORCESHOWHIDDEN = 0x10000000
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private class OpenFileName
{
internal int structSize;
internal IntPtr owner;
internal IntPtr instance;
internal string filter;
internal string customFilter;
internal int maxCustFilter;
internal int filterIndex;
internal string file;
internal int maxFile;
internal string fileTitle;
internal int maxFileTitle;
internal string initialDir;
internal string title;
internal Int16 flags;
internal Int16 fileOffset;
internal int fileExtension;
internal string defExt;
internal IntPtr custData;
internal IntPtr hook;
internal string templateName;
}
private static class NativeMethods
{
[DllImport("comdlg32.dll",
CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool
GetOpenFileName([In, Out] OpenFileName ofn);
[DllImport("comdlg32.dll",
CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool
GetSaveFileName([In, Out] OpenFileName ofn);
}
#endregion
public CommonDialog()
{
// Initialize structure that is passed to the API functions.
ofn.structSize = Marshal.SizeOf(ofn);
ofn.file = new String(new char[260]);
ofn.maxFile = ofn.file.Length;
ofn.fileTitle = new String(new char[100]);
ofn.maxFileTitle = ofn.fileTitle.Length;
}
/// <summary>
/// Display the Vista-style common Open dialog.
/// </summary>
public bool ShowOpen()
{
SetFilter();
ofn.flags = (Int16)OpenFileNameFlags.OFN_FILEMUSTEXIST;
if (Application.Current.MainWindow != null)
ofn.owner =
new WindowInteropHelper(Application.Current.MainWindow).Handle;
return NativeMethods.GetOpenFileName(ofn);
}
/// <summary>
/// Display the Vista-style common Save As dialog.
/// </summary>
public bool ShowSave()
{
SetFilter();
ofn.flags = (Int16)(OpenFileNameFlags.OFN_PATHMUSTEXIST |
OpenFileNameFlags.OFN_OVERWRITEPROMPT);
if (Application.Current.MainWindow != null)
ofn.owner = new WindowInteropHelper(
Application.Current.MainWindow).Handle;
return NativeMethods.GetSaveFileName(ofn);
}
/// <summary>
/// Set the low level filter with the filter collection.
/// </summary>
private void SetFilter()
{
StringBuilder sb = new StringBuilder();
foreach (FilterEntry entry in this.filter)
sb.AppendFormat("{0}\0{1}\0",
entry.Display, entry.Extention);
sb.Append("\0\0");
ofn.filter = sb.ToString();
}
}
}
References
The following is a list of the code that I have looked at, and in some cases, used and altered for this article:
- IanG on Tap: Flippable 3D List Items in WPF
- Paul Tallet: FishEyePanel/FanPanel - Examples of custom layout panels in WPF
So what do you think?
I would just like to ask, if you liked the article, please vote for it, and leave some comments, as it lets me know if the article was at the right level or not and whether it contained what people need to know.
Conclusion
I hope you have learned a few things reading this article.