![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Intermediate
License: The Code Project Open License (CPOL)
MyFriends : A simple contact keeper using XLINQ/LINQ/WPFBy Sacha BarberA simple contact keeper using XLINQ/LINQ/WPF |
C# (C# 3.0), Windows, .NET 3.0, .NET 3.5, WPF, LINQ, VS2008, Dev
|
||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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 am used to seeing forms with grids, list and listviews which were functional, but looked 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 a ListBox and a 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.
So 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 its totally free and that you get full support for it and even free upgrades. I couldn't believe that, but its true, i've done some homework, contacted support etc etc. The DataGrid in question is from Xceed and can be found right here. I have to say Xceed have 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 elses stuff unless it was worth while.
But fear not, its not all going to be, me being gushy about some DataGrid. Hell No. In this article I hope to demonstrate the following concepts.
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 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 assigne the following items
I guess the only place to start is at the beginning, so ill 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 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 screen shot of the flow through the various screen may be in order. I will show bigger screen shots further down the line, as I describe some of the inner working a bit better.
It can be seen that there is an initial window MaininterfaceWindow and from there you can show 3 windows (providing you are in grow-shrink UI mode) AddNewFriendControl 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 friends 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.
So thats the basic idea of what the demo app does. What the rest of this article will describe is how some of the more exotic fucntionality was achieved.
I stummbled across an excellent blog entry by a fellow call Ian g, that 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 currently control is basically rotated around the Y-axis. But how does it achieve this.
Some initial things to note:
DataTemplate DataTemplate is actually applied to a ItemsControl (Items3d in the code) ItemsControl (Items3d in the code) only ever contains one item. The contents of which are not important, its a dummy entry that simply allows the 1st 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"); thats how much we care about the contents of the actual item in the ItemsControl. The DataTemplate is where all the real work is done So thats the basics discussed, so what about this DataTemplate that achieves all this good 3D stuff for us. Well here is is. Dont worry, Im going to explain this a bit more thoroughly, as its 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 main 3D Datatemplate is where most of the action happens. And the basic idea is, that 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 propertuy for GeometryModel3D.BackMaterial. This is used within this handsome Datatemplate. The Datatemplate actually constructs a faily 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. So we can see 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 2 seperate triangles. This is how 3D works. Lets see these 2 triangles
So thats how we get the initial shape basically a square that will hold some content. So 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 the GeometryModel3D.Material is being bound to is 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 targetting various elements such as frontWrapper, backWrapper, camera, 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 either the current front conrtol shown is clicked, the current front control will gradually be rotated (around theY-Axis) and changed to invisible, and at the end of the animation cylce the other (not current) control will be shown. If your more curious about this just examine the various animations, youll see it, its fairly ok actually.
The last thing of interest within the frontHost element, is that there is a ContentPresenter which targets yet another DataTemplate for it actual ControlTemplate. Lets 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 its 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 end up being within the 3D Viewport. The same principle is applied to the BackMaterial where a seperate binding is used on a VisualBrush to the backHost element. Which in turn uses the backTemplate DataTemplate for it own ContentPresenter
The back loads the ViewAllUsersControl
And thats how the 3D DataTemplate works. Neat Huh!
Ok so weve 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 its pretty straight forward really it does the following
AddNewFriendControl that allows new friends to be added ViewAllUsersControl which shows all the friends in the Xceed WPF datagrid Thats it really, of course there are a few helper screens along the way. But in essence thats it.
So why I am talking about this in a section entitled Singleton Pattern yada yada yada, well its 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 2 copies of the controls needs be kept in synch somehow, as the user could potentially 1/2 way through an operation, decide to change the UI mode. So thats where we need the singleton pattern. Its quite a life saver actually. There is a class FriendContent.cs 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.cs class.

It can seen that there are properties available for 5 items
So it is no suprise then that the FriendContent.cs class provides these same 5 properties that may nbne 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 exists in the windows Logical Tree, as its a normal child to a Grid control, so could be accessed in code behind. But the copy of the AddNewFriendControl control that is part of the 3D DataTemplate is a little trickier, as one CANT simply refer to this by name, as its part of a controls DataTemplate so is not part of the overall logical tree.
Anyway all that aside the FriendContent.cs 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.cs 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 the correct item and chaneg its properties directly. However I also stated that one copy of the 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 tricker. Luckily we only have to care about updating these 2 controls that are part of the 3D DataTemplate when the display mode is changed to 3D. So I looked around for event that occurs whenever the user changed 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.
Lets just take a minute, what are we trying to acheive. We are trying to get the 2 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. Thats sounds easy right? Wrong, its a bit of a trick. Lets see.
I should say it took me quite a while to come up with this code, to 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, im going to have to show a portion of the 3D DataTemplate again.
It can be seen that the 1st thing to find is the Grid and then try and get the ContentPresenter for the backContent, and then from there its just a case of grabbing the ContentPresenters 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 no.
So now that we have a reference to these 2 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 use a 2nd 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 seperate 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. Ive kept my implementation the same as I originally published it, though if I was going to change I would probably go with Joshs suggestion as it makes most sense to me.
Once you have picked a directory within the OptionsWindow that has images in it, there is a additional window that is accessable from a button underneath the users image on the AddNewFriendControl. When clicked this button shows the AddFriendImageWindow which is as shown below
I make use of Paul Tallets 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 eale 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 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 2 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 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
AddNewFriendControl is clicked, a check is done to see if there is an xml in existence. If there isnt a new XML file is created using XLINQ. A new Friend object is added to a an internal collection of object used by the data grid Friend objects, append to the XML file and add a new Friend object to the internal collection of object used by the data grid. However if there is no XML, just add a new Friend object to the internal collection of object used by the data grid Friend objects to an XML file that will overwrite the existing XML file (if it exists). So thats 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 2nd singleton that was used by the ViewAllUsersControl to allow it to maintain the correct data to show in the data grid. Well thats 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 isnt finished yet. Lets follow this path. There is another class called XMLFileOperations lets 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 exceptio 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 file 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, its well cool and powerful
Like I said right at the beginning of this article, I would not normal include a 3rd 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
In fact you can totally restyle it, if thats your bag, me I just wanted to try it out for size. Basically Ive tried all the things above so ill talk about each of them in trun.
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. Anothing 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 havent 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

Thats simple isnt it, Anyway lets crack on
This is a synch, all that you have to do is create a column, and define a template for it. And 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 Templates we can simply create whatever content we want for a particular cell, and that what ive done. In this example we have an image being shown in a cell using a reflection. And this looks like the cell below
This is actually pretty easy to, as Xceed have made that pretty easy, by just creating another type of Template, an edit Template. Lets 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?
Another thing I personally like with this grid, is that it is possible to plug in validator into certain cells. Lets 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 in runtime we have a validator in action

The Xceed grid supports several different views straight out of the box, such as table and card. I have provided 2 buttons to toggle between these views. The switching between these views is easility 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 its free should not be overlooked. Ill 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 elements 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, that the play/stop/view buttons are only shown if the assocaited MediaElements 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
{
/// <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();
}
}
}
The following is a list of the code that I have looked at and in some case 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.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 18 Jan 2008 Editor: |
Copyright 2007 by Sacha Barber Everything else Copyright © CodeProject, 1999-2009 Web21 | Advertise on the Code Project |