Click here to Skip to main content
6,631,404 members and growing! (18,192 online)
Email Password   helpLost your password?
Platforms, Frameworks & Libraries » Windows Presentation Foundation » General     Intermediate License: The Code Project Open License (CPOL)

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

By Sacha Barber

A simple contact keeper using XLINQ/LINQ/WPF
C# (C# 3.0), Windows, .NET 3.0, .NET 3.5, WPF, LINQ, VS2008, Dev
Posted:1 Dec 2007
Updated:18 Jan 2008
Views:70,445
Bookmarked:169 times
Unedited contribution
Announcements
Loading...
 
Search    
Advanced Search
Add to IE Search
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
83 votes for this article.
Popularity: 8.62 Rating: 4.49 out of 5
6 votes, 7.1%
1
2 votes, 2.4%
2
5 votes, 6.0%
3
3 votes, 3.6%
4
68 votes, 81.0%
5

Contents

Introduction

The other week I was surfing the web and I came across something that perked my interest, and this article is kind of a result of what I found. Basically, I come from a database type of background, so 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.

What Is In This Article

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.

  • Some simple 3D in WPF
  • How the singleton pattern saved the day
  • Traversing the visual tree for a templated object
  • File system treeview, with lazy loading and icons
  • LINQ over objects
  • XLINQ usage
  • Some slightly sneaky value converters
  • Vista style Dialogs
There will of course be one section on the usage of the Xceed DataGrid, as I think it may help some folk out if they decide to use it in their own applications. I think there is enough here to talk about to keep people interested, at least I hope so.

The Demo App

What 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

  1. A name
  2. An email address
  3. An image
  4. A video clip
  5. A music clip

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.

3D

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:

  1. That the currently shown control is actually part of a DataTemplate
  2. That the DataTemplate is actually applied to a ItemsControl (Items3d in the code)
  3. That the 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 3D DataTemplate

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!

How the singleton pattern saved the day

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

  1. There is a Usercontrol : AddNewFriendControl that allows new friends to be added
  2. There is a Usercontrol : 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

  • Name
  • Email
  • Image Url
  • Video Url
  • Music Url

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.

Traversing the visual tree for a templated object

As I just stated, those UI elements that belong to the logical tree of the window, it is no problem to simply get a reference 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();
}

File system treeview, with lazy loading and icons

The OptionsWindow allows users to change between display styles, but it also shows a lazy loaded treeview that shows the directory structure of the host computer. This treeview can be used to pick the source directory that is used by the AddFriendImageWindow.

I have been working on this article for a while, so I split the treeview implementation into a 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.

LINQ over objects

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);

XLINQ usage

The persistence of the friends that have been added to the demo app, is done 100% using XLINQ. It works as follows

  • When the Save Friend button on the AddNewFriendControl is clicked, a check is done to see if there is an xml in existence. If there 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
  • 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 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
  • 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).

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

Getting gushy about the Xceed data grid

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

  • Create cell templates
  • Create cell edit templates
  • Create cell validators
  • Create different views

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

Create cell templates

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

Create cell editor templates

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?

Create cell validators

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

Create different view

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

Some slightly sneaky value converters

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}}" 

Vista style Dialogs

One last thing, then were are done.....I happened to notice during this application, that some of the common dialogs such as Open/Save were not displaying as my own Vista dialogs were, so I hunted around and found some code in the SDK examples that did the trick. This is shown below

using System;
using System.Windows;
using System.Windows.Interop;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace MyFriends
{
    /// <summary>

    /// One item in the common dialog filter.

    /// </summary>

    public class FilterEntry
    {
        private string display;
        private string extention;

        public string Display
        {
            get { return display; }
        }

        public string Extention
        {
            get { return extention; }
        }

        public FilterEntry(string display, string extension)
        {
            this.display = display;
            this.extention = extension;
        }
    }

    /// <summary>

    /// Displays the common Open and SaveAs dialogs using the Vista-style dialogs.

    /// </summary>

    class CommonDialog
    {
        #region fields

        // Structure used when displaying Open and SaveAs dialogs.

        private OpenFileName ofn = new OpenFileName();
        // List of filters to display in the dialog.

        private List<FilterEntry> filter = new List<FilterEntry>();
        #endregion

        #region properties

        public List<FilterEntry> Filter
        {
            get { return filter; }
        }

        public string Title
        {
            set { ofn.title = value; }
        }

        public string InitialDirectory
        {
            set { ofn.initialDir = value; }
        }

        public string DefaultExtension
        {
            set { ofn.defExt = value; }
        }

        public string FileName
        {
            get { return ofn.file; }
        }

        #endregion

        #region pinvoke details

        private enum OpenFileNameFlags
        {
            OFN_READONLY = 0x00000001,
            OFN_OVERWRITEPROMPT = 0x00000002,
            OFN_HIDEREADONLY = 0x00000004,
            OFN_NOCHANGEDIR = 0x00000008,
            OFN_SHOWHELP = 0x00000010,
            OFN_ENABLEHOOK = 0x00000020,
            OFN_ENABLETEMPLATE = 0x00000040,
            OFN_ENABLETEMPLATEHANDLE = 0x00000080,
            OFN_NOVALIDATE = 0x00000100,
            OFN_ALLOWMULTISELECT = 0x00000200,
            OFN_EXTENSIONDIFFERENT = 0x00000400,
            OFN_PATHMUSTEXIST = 0x00000800,
            OFN_FILEMUSTEXIST = 0x00001000,
            OFN_CREATEPROMPT = 0x00002000,
            OFN_SHAREAWARE = 0x00004000,
            OFN_NOREADONLYRETURN = 0x00008000,
            OFN_NOTESTFILECREATE = 0x00010000,
            OFN_NONETWORKBUTTON = 0x00020000,
            OFN_NOLONGNAMES = 0x00040000,
            OFN_EXPLORER = 0x00080000,
            OFN_NODEREFERENCELINKS = 0x00100000,
            OFN_LONGNAMES = 0x00200000,
            OFN_ENABLEINCLUDENOTIFY = 0x00400000,
            OFN_ENABLESIZING = 0x00800000,
            OFN_DONTADDTORECENT = 0x02000000,
            OFN_FORCESHOWHIDDEN = 0x10000000
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private class OpenFileName
        {
            internal int structSize;
            internal IntPtr owner;
            internal IntPtr instance;
            internal string filter;
            internal string customFilter;
            internal int maxCustFilter;
            internal int filterIndex;
            internal string file;
            internal int maxFile;
            internal string fileTitle;
            internal int maxFileTitle;
            internal string initialDir;
            internal string title;
            internal Int16 flags;
            internal Int16 fileOffset;
            internal int fileExtension;
            internal string defExt;
            internal IntPtr custData;
            internal IntPtr hook;
            internal string templateName;
        }

        private static class NativeMethods
        {
            [DllImport("comdlg32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool GetOpenFileName([In, Out] OpenFileName ofn);

            [DllImport("comdlg32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool GetSaveFileName([In, Out] OpenFileName ofn);
        }

        #endregion

        public CommonDialog()
        {
            // Initialize structure that is passed to the API functions.

            ofn.structSize = Marshal.SizeOf(ofn);
            ofn.file = new String(new char[260]);
            ofn.maxFile = ofn.file.Length;
            ofn.fileTitle = new String(new char[100]);
            ofn.maxFileTitle = ofn.fileTitle.Length;
        }

        /// <summary>

        /// Display the Vista-style common Open dialog.

        /// </summary>



        public bool ShowOpen()
        {
            SetFilter();
            ofn.flags = (Int16)OpenFileNameFlags.OFN_FILEMUSTEXIST;
            if (Application.Current.MainWindow != null)
                ofn.owner = new WindowInteropHelper(Application.Current.MainWindow).Handle;
            return NativeMethods.GetOpenFileName(ofn);
        }

        /// <summary>

        /// Display the Vista-style common Save As dialog.

        /// </summary>

        public bool ShowSave()
        {
            SetFilter();
            ofn.flags = (Int16)(OpenFileNameFlags.OFN_PATHMUSTEXIST | OpenFileNameFlags.OFN_OVERWRITEPROMPT);
            if (Application.Current.MainWindow != null)
                ofn.owner = new WindowInteropHelper(Application.Current.MainWindow).Handle;
            return NativeMethods.GetSaveFileName(ofn);
        }

        /// <summary>

        /// Set the low level filter with the filter collection.

        /// </summary>

        private void SetFilter()
        {
            StringBuilder sb = new StringBuilder();
            foreach (FilterEntry entry in this.filter)
                sb.AppendFormat("{0}\0{1}\0", entry.Display, entry.Extention);
            sb.Append("\0\0");
            ofn.filter = sb.ToString();
        }
    }
}

References

The following is a list of the code that I have looked at and in some case used and altered for this article:

So What Do You Think ?

I would just like to ask, if you liked the article please vote for it, and leave some comments, as it lets me know if the article was at the right level or not, and whether it contained what people need to know.

Conclusion

I hope you have learned a few things reading this article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Sacha Barber


Member
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Occupation: Software Developer (Senior)
Location: United Kingdom United Kingdom

Other popular Windows Presentation Foundation articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 25 of 61 (Total in Forum: 61) (Refresh)FirstPrevNext
QuestionNeed Help here. PinmemberJayze Joves17:09 16 Aug '09  
AnswerRe: Need Help here. PinmvpSacha Barber22:43 16 Aug '09  
JokeOrange PinmemberWillemToerien4:42 6 Jul '09  
GeneralRe: Orange PinmvpSacha Barber6:29 6 Jul '09  
Generalso cOOl PinmemberMember 449816922:01 9 Mar '09  
GeneralRe: so cOOl PinmvpSacha Barber23:39 9 Mar '09  
Generalhow can i invoke the flipping panel from codebehind? Pinmemberchristoph braendle14:12 1 Dec '08  
GeneralXceed Grid Pinmemberclosl13:38 26 Sep '08  
GeneralRe: Xceed Grid PinmvpSacha Barber21:36 26 Sep '08  
QuestionI have a problem deleting Users Pinmemberzuare0:04 26 May '08  
AnswerRe: I have a problem deleting Users PinmvpSacha Barber11:06 26 May '08  
GeneralYou rock Pinmemberamiah11:49 26 Apr '08  
GeneralRe: You rock PinmvpSacha Barber2:37 28 Apr '08  
Generalhelp Pinmemberdjsoul2:50 12 Mar '08  
GeneralRe: help PinmvpSacha Barber3:03 12 Mar '08  
GeneralRe: help Pinmemberdjsoul10:34 12 Mar '08  
GeneralRe: help PinmvpSacha Barber11:33 12 Mar '08  
GeneralRe: help Pinmemberdjsoul11:47 12 Mar '08  
GeneralRe: help PinmvpSacha Barber1:52 13 Mar '08  
GeneralRe: help Pinmemberdjsoul3:29 13 Mar '08  
GeneralRe: help PinmvpSacha Barber3:40 13 Mar '08  
GeneralRe: help Pinmemberdjsoul6:49 13 Mar '08  
GeneralRe: help PinmvpSacha Barber7:32 13 Mar '08  
GeneralNice Article PinmemberDreamzor0:15 5 Feb '08  
GeneralRe: Nice Article PinmvpSacha Barber0:50 5 Feb '08  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin 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