Contents
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.
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.
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.
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 an ItemsControl
(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 the ItemsControl
to be assigned the 3D flipping DataTemplate
. In fact, in the code-behind, you will find the line items3d.Items.Add("dont care");
- that's how much we care about the contents of the actual item in the ItemsControl
. The DataTemplate
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">
-->
-->
<Grid Margin="0,0,0,0" Width="800"
Height="800" HorizontalAlignment="Center"
VerticalAlignment="Center">
-->
<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>
-->
<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>
-->
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush
Visual="{Binding ElementName=frontHost}" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
-->
<GeometryModel3D.BackMaterial>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush
Visual="{Binding ElementName=backHost}">
<VisualBrush.RelativeTransform>
-->
<ScaleTransform ScaleX="-1"
CenterX="0.5" />
</VisualBrush.RelativeTransform>
</VisualBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.BackMaterial>
-->
<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>
-->
<Border x:Name="frontWrapper">
-->
<Border x:Name="frontHost" 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="backWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:1"
Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
-->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="frontWrapper"
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="frontWrapper"
Storyboard.TargetProperty="Opacity" />
-->
<DoubleAnimation BeginTime="0:0:1.05"
Duration="0:0:0.05" To="1"
Storyboard.TargetName="backWrapper"
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="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 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:
<!---->
<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:
<!---->
<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>
.......
.......
.......
.......
<!---->
<Border x:Name="frontWrapper">
<!---->
<Border x:Name="frontHost" 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="backWrapper"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:1"
Value="{x:Static Visibility.Visible}"/>
</ObjectAnimationUsingKeyFrames>
<!---->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="frontWrapper"
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="frontWrapper"
Storyboard.TargetProperty="Opacity" />
<!---->
<DoubleAnimation BeginTime="0:0:1.05"
Duration="0:0:0.05" To="1"
Storyboard.TargetName="backWrapper"
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="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?
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
- Email
- 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.
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)
{
addfriendsControl3D = sender as AddNewFriendControl;
addfriendsControl3D.ReInitialise();
DependencyObject item = null;
foreach (object dataitem in items3d.Items)
{
item = items3d.ItemContainerGenerator.ContainerFromItem(dataitem);
int count = VisualTreeHelper.GetChildrenCount(item);
for (int i = 0; i < count; i++)
{
DependencyObject itemFetched = VisualTreeHelper.GetChild(item, i);
if (itemFetched is Grid)
{
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;
if (friendContent.PhotoUrl != null)
if (!friendContent.PhotoUrl.Equals(string.Empty))
photoSrc.Source =
new BitmapImage(new Uri(friendContent.PhotoUrl));
if (friendContent.VideoUrl != null)
if (!friendContent.VideoUrl.Equals(string.Empty))
videoSrc.Source = new Uri(friendContent.VideoUrl);
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();
}
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.
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);
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);
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 new Friend
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 new Friend
object to the internal collection of objects used by the data grid. However, if there is no XML, just add a new Friend
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 (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");
}
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();
}
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.
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:
<!---->
<xcdg:Column FieldName="PhotoUrl" VisiblePosition="3" Visible="True">
<!---->
<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.
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}}"
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
{
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;
}
}
class CommonDialog
{
#region fields
private OpenFileName ofn = new OpenFileName();
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()
{
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;
}
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);
}
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);
}
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();
}
}
}
The following is a list of the code that I have looked at, and in some cases, used and altered for this article:
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.
I hope you have learned a few things reading this article.