Click here to Skip to main content
Click here to Skip to main content

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

, 18 Jan 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
A simple contact keeper using XLINQ/LINQ/WPF.

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 I am used to seeing forms with grids, lists, and listviews which are functional but look pretty boring. This is kind of why I like WPF so much, as you can make functional apps, but make them look really sexy. Anyone that has done any WPF will probably know that there are a couple of data type controls such as ListBox and ListView, and with some clever binding and some templating/styling, we could probably make these look like a grid. But the truth is that there simply is no DataGrid or anything like that.

That short fall is kind of what this article is about, but I hope this article will have enough extra meat to keep people interested.

What is in this article

I've just mentioned that this article will feature a DataGrid for WPF, and it will. This is the little gem that I mentioned that I found while trawling the internet. And the best part is that it is totally free and that you get full support for it and even free upgrades. I couldn't believe that, but it's true. I've done some homework, contacted support etc. The DataGrid in question is from Xceed and can be found right here. I have to say Xceed has done a bang up job in my opinion. I mean, I would not be wasting my time (which is Xceedingly cheap in case you were wondering) writing about someone else's stuff unless it was worth while.

But fear not, it's not all going to be me being gushy about some DataGrid. Hell no. In this article, I hope to demonstrate the following concepts:

  • Some simple 3D in WPF
  • How the Singleton pattern saved the day
  • Traversing the visual tree for a templated object
  • File system treeview, with lazy loading and icons
  • LINQ over objects
  • XLINQ usage
  • Some slightly sneaky value converters
  • Vista style dialogs

There will, of course, be one section on the usage of the Xceed DataGrid, as I think it may help some folk out if they decide to use it in their own applications. I think there is enough here to talk about to keep people interested, at least I hope so.

The demo app

What does the demo app actually do then?

The attached demo application is like a mini Outlook contact keeper; you can manage your friends by adding, removing, and updating them. For each friend, it it possible to assign the following items:

  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 I'll just jump in there.

I set out on this one to create a combination of things. I wanted a UI that could show different components by growing and shrinking them to come into view (kind of like the Infragistics WPF showcase Tangerine), but I also wanted to use a 3D display option. These UI modes are mutually exclusive; by that, I mean if you are in grow-shrink mode, you cant use 3D methods, and vice versa. The selection of the current mode is from within an OptionsWindow, but more on that later. I also wanted the UI to be able to show a grid of data.

I think a nice little screenshot of the flow through the various screens may be in order. I will show bigger screenshots further down the line, as I describe some of the inner workings a bit better.

It can be seen that there is an initial window MaininterfaceWindow, and from there, you can show three windows (providing you are in grow-shrink UI mode). AddNewFriendControl is from where you may choose to add an image for your friend by using the AddFriendImageWindow. From the MaininterfaceWindow, it is also possible to show the ViewAllUsersControl which shows all your friends in a data grid, and from there, providing there is a video assigned to your friend, you will be able to show the VideoViewerWindow to view your friend's associated video. There is also an OptionsWindow from where you can pick which UI style you want, Grow-Shrink or 3D, and also pick the folder that is used for images by the AddFriendImageWindow.

That's the basic idea of what the demo app does. What the rest of this article will describe is how some of the more exotic functionality was achieved.

3D

I stumbled across an excellent blog entry by a fellow call Ian G, who had this blog entry about 3D flipping listboxes. Ian simply posted some code, but there was no explanation of how it worked, and it just intrigued me. So I had to rip it to pieces and find out how it worked. The result of this analysis is described below.

OK, so what the heck is all this 3D stuff you're on about Sacha? Well, quite simply, the UI allows a 3D style interaction, as shown below, where to toggle between adding new friends (AddNewFriendControl) and viewing all friends (ViewAllUsersControl), the user is able to flip the currently displayed item in a 3D viewport.

The current control is basically rotated around the Y-axis. But how does it achieve this?

Some initial things to note:

  1. The currently shown control is actually part of a DataTemplate.
  2. The DataTemplate is actually applied to an ItemsControl (Items3d in the code).
  3. The ItemsControl (Items3d in the code) only ever contains one item. The contents of which are not important; it's a dummy entry that simply allows the first item within the ItemsControl to be assigned the 3D flipping DataTemplate. In fact, in the code-behind, you will find the line items3d.Items.Add("dont care"); - that's how much we care about the contents of the actual item in the ItemsControl. The DataTemplate is where all the real work is done.

That's the basics discussed. What about this DataTemplate that achieves all this good 3D stuff for us? Well, here it is. Don't worry, I'm going to explain this a bit more thoroughly, as it's fairly complicated.

<DataTemplate x:Key="frontTemplate">
    <StackPanel Orientation="Vertical">
        <local:AddNewFriendControl x:Name="addFriendsControl3d" 
           Width="750" Height="500" 
           SizeChanged="AddNewFriendControl_SizeChanged"/>
        <Border Height="20" Background="Yellow" 
               Width="750" HorizontalAlignment="Center" 
               CornerRadius="5,5,5,5" BorderBrush="#FFD0601D">
            <TextBlock TextAlignment="Center" 
               FontFamily="Tahoma" FontSize="11" 
               Text="Click here to see all you friends"/>
        </Border>
    </StackPanel>
</DataTemplate>
<DataTemplate x:Key="backTemplate">
    <StackPanel Orientation="Vertical">
        <local:ViewAllUsersControl x:Name="viewFriendsControl3d" 
               Width="750" Height="500"/>
        <Border Height="20" Background="Yellow" 
                Width="750" HorizontalAlignment="Center" 
                CornerRadius="5,5,5,5" BorderBrush="#FFD0601D">
            <TextBlock TextAlignment="Center" 
                FontFamily="Tahoma" FontSize="11" 
                Text="Click here to add new friends"/>
        </Border>
    </StackPanel>
</DataTemplate>

<DataTemplate x:Key="flipItemTemplate">

    <!-- Note: Camera setup only works when this is square. -->
    <!--<Grid Width="800" Height="800" 
               HorizontalAlignment="Center" 
               VerticalAlignment="Center">-->
    <Grid Margin="0,0,0,0" Width="800" 
              Height="800" HorizontalAlignment="Center" 
              VerticalAlignment="Center">

    <!-- Provides 3D rotation transition. 
            Hidden except for when animation is active. -->
        <Viewport3D Grid.Column="0" x:Name="vp3D" 
                  Visibility="Hidden" Width="Auto" 
                  Height="Auto" Margin="0,0,0,0" >
            <Viewport3D.Camera>
                <PerspectiveCamera x:Name="camera" 
                   Position="0,0,0.5" 
                   LookDirection="0,0,-1" 
                   FieldOfView="90" />
            </Viewport3D.Camera>

            <Viewport3D.Children>

                <ModelVisual3D>
                    <ModelVisual3D.Content>
                        <Model3DGroup>
                            <DirectionalLight Color="#444" 
                                     Direction="0,0,-1" />
                            <AmbientLight Color="#BBB" />
                        </Model3DGroup>
                    </ModelVisual3D.Content>
                </ModelVisual3D>
                <ModelVisual3D>
                    <ModelVisual3D.Content>
                        <GeometryModel3D>

                            <!-- Simple flat, square surface -->
                            <GeometryModel3D.Geometry>
                                <MeshGeometry3D
                                   TriangleIndices="0,1,2    2,3,0"
                                   TextureCoordinates="0,1 1,1 1,0 0,0"
                                   Positions="-0.5,-0.5,0     0.5,-0.5,0
                                                  0.5,0.5,0    -0.5,0.5,0" />
                            </GeometryModel3D.Geometry>

                            <!-- Front of shape shows the content of 'frontHost' -->
                            <GeometryModel3D.Material>
                                <DiffuseMaterial>
                                    <DiffuseMaterial.Brush>
                                        <VisualBrush 
                                           Visual="{Binding ElementName=frontHost}" />
                                    </DiffuseMaterial.Brush>
                                </DiffuseMaterial>
                            </GeometryModel3D.Material>

                            <!-- Back of shape shows the content of 'backHost' -->
                            <GeometryModel3D.BackMaterial>
                                <DiffuseMaterial>
                                    <DiffuseMaterial.Brush>
                                        <VisualBrush 
                                              Visual="{Binding ElementName=backHost}">
                                            <VisualBrush.RelativeTransform>
                                                <!-- By default, this would come out 
                                                   backwards because we're on the
                                                   back on the shape. 
                                                   Flip it to make it right. -->
                                                <ScaleTransform ScaleX="-1" 
                                                         CenterX="0.5" />
                                            </VisualBrush.RelativeTransform>
                                        </VisualBrush>
                                    </DiffuseMaterial.Brush>
                                </DiffuseMaterial>
                            </GeometryModel3D.BackMaterial>

                            <!-- Rotation transform used for transition. -->
                            <GeometryModel3D.Transform>
                                <RotateTransform3D>
                                    <RotateTransform3D.Rotation>
                                        <AxisAngleRotation3D 
                                           x:Name="rotate" 
                                           Axis="0,1,0" Angle="0" />
                                    </RotateTransform3D.Rotation>
                                </RotateTransform3D>
                            </GeometryModel3D.Transform>
                        </GeometryModel3D>
                    </ModelVisual3D.Content>
                </ModelVisual3D>
            </Viewport3D.Children>
        </Viewport3D>

        <!-- We use a pair of nested Borders to wrap the content that's going 
                to go on each side of the rotating model.
                The reason is that we need to be able to fade these real bits 
                of UI in and out as we transition from front to back, but we need 
                to make sure the VisualBrush in the 3D model doesn't also 
                get faded out. So the VisualBrush uses the inner Border, 
                while the fade is applied to the outer one.
-->
        <Border x:Name="frontWrapper">
            <!-- Note, it's important that this element has visuals 
                     that completely fill the space, as otherwise it messes 
                     with the VisualBrush's size in the 3D model. Setting the background
                     has that effect, even a transparent one. -->
            <Border x:Name="frontHost" Background="Transparent">
                <Border.Triggers>
                    <EventTrigger RoutedEvent="Grid.MouseDown">
                        <BeginStoryboard>
                            <Storyboard>
                                <!-- Make the Viewport3D visible only 
                                           for the duration of the rotation. -->
                                <ObjectAnimationUsingKeyFrames
                                        Storyboard.TargetName="vp3D"
                                        Storyboard.TargetProperty="Visibility">
                                    <DiscreteObjectKeyFrame KeyTime="0:0:0" 
                                         Value="{x:Static Visibility.Visible}" />
                                    <DiscreteObjectKeyFrame KeyTime="0:0:1.1" 
                                         Value="{x:Static Visibility.Hidden}" />
                                </ObjectAnimationUsingKeyFrames>

                                <!-- Make the background element visible. 
                                       (It won't actually appear 
                                            until it is faded in right at the end 
                                            of the animation.) -->
                                <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="backWrapper"
                                        Storyboard.TargetProperty="Visibility">
                                    <DiscreteObjectKeyFrame KeyTime="0:0:1" 
                                            Value="{x:Static Visibility.Visible}"/>
                                </ObjectAnimationUsingKeyFrames>

                                <!-- Hide the foreground element. 
                                      It will already be invisible 
                                      by this time because we fade 
                                      it out right at the start 
                                      of the animation. 
                                      However, until we set its Visibility 
                                      to Hidden, it will still be 
                                      visible to the mouse... -->
                                <ObjectAnimationUsingKeyFrames 
                                       Storyboard.TargetName="frontWrapper"
                                       Storyboard.TargetProperty="Visibility">
                                    <DiscreteObjectKeyFrame KeyTime="0:0:0.05" 
                                        Value="{x:Static Visibility.Hidden}" />
                                </ObjectAnimationUsingKeyFrames>

                                <!-- Fade the front wrapper out. 
                                   The Viewport3D is behind us, 
                                   so it'll fade into view at this point. The reason 
                                   for fading is to avoid a visible step as we
                                   switch from the real UI to the copy 
                                   projected onto the 3D model. -->
                                <DoubleAnimation To="0" 
                                       Duration="0:0:0.05"
                                       Storyboard.TargetName="frontWrapper"
                                       Storyboard.TargetProperty="Opacity" />

                                <!-- Fade the back wrapper in. Once the spin 
                                      completes, we fade the real back UI
                                      in over the Viewport3D - using a fade to avoid 
                                      a sudden jolt between the slightly fuzzy 3D look 
                                      and the real UI. -->
                                <DoubleAnimation BeginTime="0:0:1.05" 
                                      Duration="0:0:0.05" To="1"
                                      Storyboard.TargetName="backWrapper"
                                      Storyboard.TargetProperty="Opacity" />

                                <!-- 3D animation. Move the camera out slightly 
                                        as we spin, so the model fits entirely
                                        within the field of view. 
                                        Rotate the model 180 degrees. -->
                                <Point3DAnimation To="0,0,1.1" From="0,0,0.5"
                                   BeginTime="0:0:0.05" Duration="0:0:0.5" 
                                   AutoReverse="True" DecelerationRatio="0.3"
                                   Storyboard.TargetName="camera"
                                   Storyboard.TargetProperty=
                                      "(PerspectiveCamera.Position)" />
                                <DoubleAnimation From="0" To="180" 
                                       AccelerationRatio="0.3" 
                                       DecelerationRatio="0.3"
                                       BeginTime="0:0:0.05" 
                                       Duration="0:0:1"
                                       Storyboard.TargetName="rotate"
                                       Storyboard.TargetProperty="Angle" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Border.Triggers>
                <ContentPresenter  Content="{Binding}" 
                  ContentTemplate="{StaticResource frontTemplate}" 
                  VerticalAlignment="Center"/>
            </Border>
        </Border>
        <Border x:Name="backWrapper" Grid.Column="0" 
                    Visibility="Hidden" Opacity="0">
            <Border x:Name="backHost" Background="Transparent">
                <Border.Triggers>
                    <EventTrigger RoutedEvent="Grid.MouseDown">
                        <BeginStoryboard>
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames 
                                        Storyboard.TargetName="vp3D"
                                        Storyboard.TargetProperty="Visibility">
                                    <DiscreteObjectKeyFrame KeyTime="0:0:0" 
                                        Value="{x:Static Visibility.Visible}" />
                                    <DiscreteObjectKeyFrame KeyTime="0:0:1.1" 
                                        Value="{x:Static Visibility.Hidden}" />
                                </ObjectAnimationUsingKeyFrames>

                                <ObjectAnimationUsingKeyFrames
                                        Storyboard.TargetName="frontWrapper"
                                        Storyboard.TargetProperty="Visibility">
                                    <DiscreteObjectKeyFrame KeyTime="0:0:1" 
                                        Value="{x:Static Visibility.Visible}"/>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames 
                                       Storyboard.TargetName="backWrapper"
                                       Storyboard.TargetProperty="Visibility">
                                    <DiscreteObjectKeyFrame KeyTime="0:0:0.05" 
                                          Value="{x:Static Visibility.Hidden}" />
                                </ObjectAnimationUsingKeyFrames>

                                <DoubleAnimation To="0" Duration="0:0:0.05"
                                       Storyboard.TargetName="backWrapper"
                                       Storyboard.TargetProperty="Opacity" />
                                <DoubleAnimation BeginTime="0:0:1.05" 
                                       Duration="0:0:0.05"
                                       Storyboard.TargetName="frontWrapper"
                                       Storyboard.TargetProperty="Opacity" />

                                <Point3DAnimation To="0,0,1.1" From="0,0,0.5"
                                     BeginTime="0:0:0.05" Duration="0:0:0.5" 
                                     AutoReverse="True" DecelerationRatio="0.3"
                                     Storyboard.TargetName="camera"
                                     Storyboard.TargetProperty=
                                        "(PerspectiveCamera.Position)" />
                                <DoubleAnimation From="180" To="360" 
                                      AccelerationRatio="0.3" DecelerationRatio="0.3"
                                      BeginTime="0:0:0.05" Duration="0:0:1"
                                      Storyboard.TargetName="rotate"
                                      Storyboard.TargetProperty="Angle" />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </Border.Triggers>
                <ContentPresenter x:Name="backContent" Content="{Binding}" 
                  ContentTemplate="{StaticResource backTemplate}" 
                  VerticalAlignment="Center"/>
            </Border>
        </Border>
    </Grid>
</DataTemplate>

So how does this all work? The basic idea is as follows:

The 3D DataTemplate

The main 3D DataTemplate is where most of the action happens. And the basic idea is, there is a Viewport3D which provides a rendering surface for 3-D visual content. The Viewport3D holds both a Viewport3D.Camera and the initial GeometryModel3D, which is the 3D model comprised of a MeshGeometry3D and a Material. 3D in WPF also exposes a property for GeometryModel3D.BackMaterial. This is used within this handsome DataTemplate. The DataTemplate actually constructs a fairly simple MeshGeometry3D as follows:

<!-- Simple flat, square surface -->
<GeometryModel3D.Geometry>
    <MeshGeometry3D
         TriangleIndices="0,1,2    2,3,0"
         TextureCoordinates="0,1 1,1 1,0 0,0"
         Positions="-0.5,-0.5,0     0.5,-0.5,0    0.5,0.5,0    -0.5,0.5,0" />
</GeometryModel3D.Geometry>

Perhaps this could do with a little explanation. The Positions property is the positions in 3D space X, Y, Z planes. We can see that if this were mapped out, we would get something like:

And the TriangleIndices property is the indices of the triangles that make up the GeometryModel3D.Geometry, in this case a simple square, which is made from two separate triangles. This is how 3D works. Let's see these two triangles:

That's how we get the initial shape, basically a square that will hold some content. How about the content? Where does that come from?

Recall, I said that this 3D Datatemplate actually allows us to rotate around the Y-axis, so there should be a front and back. Which indeed there is:

The front section is made up as follows:

<!-- Simple flat, square surface -->
<GeometryModel3D.Geometry>
    <MeshGeometry3D
         TriangleIndices="0,1,2    2,3,0"
         TextureCoordinates="0,1 1,1 1,0 0,0"
         Positions="-0.5,-0.5,0     0.5,-0.5,0    0.5,0.5,0    -0.5,0.5,0" />
</GeometryModel3D.Geometry>

.......
.......
.......
.......

<!-- We use a pair of nested Borders to wrap the content that's going to go on
each side of the rotating model.
The reason is that we need to be able to fade these real bits of UI in and out
as we transition from front to back, but we need to make sure the VisualBrush
in the 3D model doesn't also get faded out. So the VisualBrush uses the inner
Border, while the fade is applied to the outer one.
-->
<Border x:Name="frontWrapper">
    <!-- Note, it's important that this element 
      has visuals that completely fill the space, as
      otherwise it messes with the VisualBrush's size 
      in the 3D model. Setting the background
      has that effect, even a transparent one. -->
    <Border x:Name="frontHost" Background="Transparent">
        <Border.Triggers>
            <EventTrigger RoutedEvent="Grid.MouseDown">
                <BeginStoryboard>
                    <Storyboard>
                        <!-- Make the Viewport3D visible only 
                               for the duration of the rotation. -->
                        <ObjectAnimationUsingKeyFrames
                                 Storyboard.TargetName="vp3D"
                                 Storyboard.TargetProperty="Visibility">
                            <DiscreteObjectKeyFrame KeyTime="0:0:0" 
                               Value="{x:Static Visibility.Visible}" />
                            <DiscreteObjectKeyFrame KeyTime="0:0:1.1" 
                               Value="{x:Static Visibility.Hidden}" />
                        </ObjectAnimationUsingKeyFrames>

                        <!-- Make the background element visible.
                            (It won't actually 
                             appear until it is faded in right 
                             at the end of the animation.) -->
                        <ObjectAnimationUsingKeyFrames 
                               Storyboard.TargetName="backWrapper"
                               Storyboard.TargetProperty="Visibility">
                            <DiscreteObjectKeyFrame KeyTime="0:0:1" 
                               Value="{x:Static Visibility.Visible}"/>
                        </ObjectAnimationUsingKeyFrames>

                        <!-- Hide the foreground element. 
                           It will already 
                           be invisible by this time because 
                           we fade it out right 
                           at the start of the animation. 
                           However, until we set its Visibility 
                           to Hidden, it will still be visible 
                           to the mouse... -->
                        <ObjectAnimationUsingKeyFrames 
                               Storyboard.TargetName="frontWrapper"
                               Storyboard.TargetProperty="Visibility">
                            <DiscreteObjectKeyFrame KeyTime="0:0:0.05" 
                                 Value="{x:Static Visibility.Hidden}" />
                        </ObjectAnimationUsingKeyFrames>


                        <!-- Fade the front wrapper out. The Viewport3D 
                                is behind us, so it'll fade into view at this point. 
                                The reason for fading is to avoid a visible step as we
                                switch from the real UI 
                                to the copy projected onto the 3D model. -->
                        <DoubleAnimation To="0" 
                            Duration="0:0:0.05"
                            Storyboard.TargetName="frontWrapper"
                            Storyboard.TargetProperty="Opacity" />

                        <!-- Fade the back wrapper in. Once the spin completes, 
                                we fade the real back UI 
                                in over the Viewport3D - using a fade 
                                to avoid a sudden jolt between the slightly 
                                fuzzy 3D look and the real UI. -->
                        <DoubleAnimation BeginTime="0:0:1.05" 
                               Duration="0:0:0.05" To="1"
                               Storyboard.TargetName="backWrapper"
                               Storyboard.TargetProperty="Opacity" />

                        <!-- 3D animation. Move the camera out slightly as we spin, 
                                so the model fits entirely within the field of view. 
                                Rotate the model 180 degrees. -->
                        <Point3DAnimation To="0,0,1.1" From="0,0,0.5"
                               BeginTime="0:0:0.05" Duration="0:0:0.5" 
                               AutoReverse="True" DecelerationRatio="0.3"
                               Storyboard.TargetName="camera"
                               Storyboard.TargetProperty=
                                     "(PerspectiveCamera.Position)" />
                        <DoubleAnimation From="0" To="180" 
                          AccelerationRatio="0.3" DecelerationRatio="0.3"
                          BeginTime="0:0:0.05" Duration="0:0:1"
                          Storyboard.TargetName="rotate"
                          Storyboard.TargetProperty="Angle" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Border.Triggers>
        <ContentPresenter  Content="{Binding}" 
          ContentTemplate="{StaticResource frontTemplate}" 
          VerticalAlignment="Center"/>
    </Border>
</Border>

Where the GeometryModel3D.Material uses a VisualBrush that is bound to an existing Element within the main DataTemplate. This can be seen on the line:

<VisualBrush Visual="{Binding ElementName=frontHost}" />

Where the element GeometryModel3D.Material is being bound to its frontHost. If we then dig a little deeper and look at the actual element frontHost (shown above), we can see that its a Border that holds various animations targeting various elements such as frontWrapper, backWrapper, camera, and rotate. To perform these animations, several different types of animations have been used; there are ObjectAnimationUsingKeyFrames, DoubleAnimation, Point3DAnimation, all of which target different properties within the main DataTemplate that allow the 3D model to be rotated. The basic idea with the various animations is that when the current front control shown is clicked, the current front control will gradually be rotated (around the Y-axis) and changed to invisible, and at the end of the animation cycle, the other (not current) control will be shown. If you are more curious about this, just examine the various animations, you will see it, it's fairly OK actually.

The last thing of interest within the frontHost element is that there is a ContentPresenter which targets yet another DataTemplate for its actual ControlTemplate. Let's see this:

<ContentPresenter 
   Content="{Binding}" ContentTemplate="{StaticResource frontTemplate}" 
   VerticalAlignment="Center"/>

And, if we look at the frontTemplate DataTemplate (which was right at the top of the main DataTempate full source code), we can see that it actually uses an instance of a AddNewFriendControl which is the front control that you see.

<DataTemplate x:Key="frontTemplate">
    <StackPanel Orientation="Vertical">
        <local:AddNewFriendControl x:Name="addFriendsControl3d" 
           Width="750" Height="500" 
           SizeChanged="AddNewFriendControl_SizeChanged"/>
        <Border Height="20" Background="Yellow" Width="750" 
                HorizontalAlignment="Center" 
                CornerRadius="5,5,5,5" BorderBrush="#FFD0601D">
            <TextBlock TextAlignment="Center" 
               FontFamily="Tahoma" FontSize="11" 
               Text="Click here to see all you friends"/>
        </Border>
    </StackPanel>
</DataTemplate>

This is how the AddNewFriendControl ends up being within the 3D Viewport. The same principle is applied to the BackMaterial where a separate binding is used on a VisualBrush to the backHost element. Which in turn uses the backTemplate DataTemplate for its own ContentPresenter.

The back loads the ViewAllUsersControl:

And that's how the 3D DataTemplate works. Neat, huh?

How the Singleton pattern saved the day

OK, so we have gone through some of the 3D stuff, which is really just talking about how the UI works in one mode. But what does the UI actually do? Well, it's pretty straightforward really; it does the following:

  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.

That's it really; of course, there are a few helper screens along the way. But in essence, that's it.

So, why am I talking about this in a section entitled Singleton Pattern yada yada yada? Well, it's like this. In the application, there is a concept of different types of display mode. You can either be in grow and shrink mode, in which case, a Grid (gridHolder) that is normally hidden is moved in to be a child element of the main display Grid (mainGrid), and the 3D ItemsControl (items3d) is removed as a child from the main display Grid (mainGrid), and vice versa. And as we now know, the 3D DataTemplate that we just discussed above also contains a copy of both a AddNewFriendControl and a ViewAllUsersControl. So surely, the content of these two copies of the controls needs be kept in synch somehow, as the user could potentially, half way through an operation, decide to change the UI mode. So that's where we need the Singleton pattern. It's quite a life saver actually. There is a class FriendContent which provides the Singleton content for the AddNewFriendControl. This is a simple class that simply stores values for all the possible entries within the AddNewFriendControl. If we have a look at the AddNewFriendControl, it may be clearer to see what properties are catered for within the FriendContent class.

It can be seen that there are properties available for five items:

  • Name
  • Email
  • Image URL
  • Video URL
  • Music URL

So, it is no surprise then that the FriendContent class provides these same five properties that may be used by both AddNewFriendControl controls, the one shown in grow-shrink mode and the one used in the 3D DataTemplate discussed earlier. You see the grow-shrink copy of the AddNewFriendControl controls exist in the Windows Logical Tree, as it's a normal child to a Grid control, so could be accessed in the code-behind. But the copy of the AddNewFriendControl control that is part of the 3D DataTemplate is a little trickier, as one can not simply refer to this by name, as it's part of a control's DataTemplate and so is not part of the overall logical tree.

Anyway, all that aside, the FriendContent class looks like this, which I think is self-explanatory. Oh, one thing to note is that this class is using the new C# syntax for automatic properties. Josh Smith raised an interesting issue about this style of creating properties in this thread, which I urge you all to read.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyFriends
{
    public class FriendContent
    {
        public string FriendName { get; set; }
        public string FriendEmail { get; set; }
        public string PhotoUrl { get; set; }
        public string VideoUrl { get; set; }
        public string MusicUrl { get; set; }
        private static FriendContent instance;

        private FriendContent()
        {
        }

        public void Reset()
        {
            FriendName = string.Empty;
            FriendEmail = string.Empty;
            PhotoUrl = string.Empty;
            VideoUrl = string.Empty;
            MusicUrl = string.Empty;
        }

        public static FriendContent Instance()
        {
            if (instance == null)
            {
                instance = new FriendContent();
            }
            return instance;
        }
    }
}

So the idea is that, we use this Singleton class to tell the AddNewFriendControl to update their content based on the single set of values within the FriendContent class.

Traversing the visual tree for a templated object

As I just stated, those UI elements that belong to the logical tree of the window, it is no problem to simply get a reference to the correct item and change its properties directly. However, I also stated that one copy of this is actually part of a 3D DataTemplate, which is applied to the single (Dummy) item in an ItemsControl. So getting to them is a little bit trickier. Luckily, we only have to care about updating these two controls that are part of the 3D DataTemplate when the display mode is changed to 3D. So, I looked around for an event that occurs whenever the user changes to 3D mode. As luck would have it, whenever the display mode is changed to 3D, I noticed that the SizeChanged event of the AddNewFriendControl got fired. Excellent, so we can use that to update the content of both of the controls within the 3D DataTemplate.

Let's just take a minute. What are we trying to achieve? We are trying to get the two controls that are part of the 3D DataTemplate to update to the latest content that will have been filled in on the grow-shrink copies of the AddNewFriendControl and the ViewAllUsersControl controls. But in order to do this, we are going to need a reference to both of these controls in the code-behind file. That sounds easy, right? Wrong, it's a bit of a trick. Let's see. I should say it took me quite a while to come up with this code, so read it carefully dear reader.

void AddNewFriendControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
    //obtaining the AddNewFriendControl is easy just 
    //use the sender and get it to ReInitialise
    //which will fetch the latest content from the FriendContent singleton
    addfriendsControl3D = sender as AddNewFriendControl;
    addfriendsControl3D.ReInitialise();

    //Obtaining an instance of the ViewAllUsersControl
    //is a little tricker, as we need to
    //find it in the DataTemplate itself,
    //which means we need to walk its VisualTree

    DependencyObject item = null;
    //there will be only 1 item, we are simply using
    //the item as a sneaky way to apply
    //out custom 3d tempplate

    foreach (object dataitem in items3d.Items)
    {
        //get the UIElement for the ItemsControl item 
        item = items3d.ItemContainerGenerator.ContainerFromItem(dataitem);
        int count = VisualTreeHelper.GetChildrenCount(item);
        for (int i = 0; i < count; i++)
        {
            DependencyObject itemFetched = VisualTreeHelper.GetChild(item, i);
            //look for a grid, which is the one
            //we need to allow use to find the relevant
            //ContentPresenter that hosts our ViewAllUsersControl
            if (itemFetched is Grid)
            {
                //do back content, and make sure all properties are copies across
                ContentPresenter cp = (itemFetched as Grid).FindName(
                                         "backContent") as ContentPresenter;
                DataTemplate myDataTemplate = cp.ContentTemplate;
                ViewAllUsersControl viewUsers = (ViewAllUsersControl)
                  myDataTemplate.FindName("viewFriendsControl3d", cp);
                viewUsers.Height = (sender as AddNewFriendControl).Height;
                viewUsers.DataBind();
                return;
            }
        }
    }
}

I think for this section of code to make sense, I'm going to have to show a portion of the 3D DataTemplate again.

It can be seen that the first thing to find is the Grid, and then try and get the ContentPresenter for the backContent, and then from there, it's just a case of grabbing the 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, right?

So now that we have a reference to these two controls within the 3D DataTemplate, what do we do with them to get them to update their content? Well, that part is actually easy. In the case of the AddNewFriendControl, we simply call the ReInitialise() method, which is as follows:

public void ReInitialise()
{
    friendContent = FriendContent.Instance();
    initialising = true;
    txtFriendName.Text = friendContent.FriendName;
    txtEmail.Text = friendContent.FriendEmail;
    //photo

    if (friendContent.PhotoUrl != null)
        if (!friendContent.PhotoUrl.Equals(string.Empty))
            photoSrc.Source = 
               new BitmapImage(new Uri(friendContent.PhotoUrl));
    //video

    if (friendContent.VideoUrl != null)
        if (!friendContent.VideoUrl.Equals(string.Empty))
            videoSrc.Source = new Uri(friendContent.VideoUrl);
    //music

    if (friendContent.MusicUrl != null)
        if (!friendContent.MusicUrl.Equals(string.Empty))
            musicSrc.Source = new Uri(friendContent.MusicUrl);
    initialising = false;
}

Which, as you can see, uses the FriendContent singleton we talked about earlier. In the case of the ViewAllUsersControl control, we simply call the DataBind() method which will cause the Xceed WPF data grid to rebind its contents. This is shown below, and uses a second singleton FriendsList that will be discussed later:

public void DataBind()
{
    dgFriends.ItemsSource = FriendsList.Instance();
}

File system treeview, with lazy loading and icons

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

I have been working on this article for a while, so I split the treeview implementation into a separate article, which is described here, which was published some time ago. Josh Smith, being Josh (which is cool) had a suggestion with this article, and posted an alternative approach which is published here, and then rather amusingly, Karl Shiflett also had an alternative approach which he published here. So there you have it, you are now spoilt for choice. I've kept my implementation the same as I originally published it, though if I was going to change, I would probably go with Josh's suggestion as it makes most sense to me.

LINQ over objects

Once you have picked a directory within the OptionsWindow that has images in it, there is an additional window that is accessible from a button underneath the user's image on the AddNewFriendControl. When clicked, this button shows the AddFriendImageWindow, which is as shown below:

I make use of Paul Tallet's excellent Fisheye panel on this page. But I also allow the user to scroll through the image folder selected on the OptionsWindow using LINQ as follows:

private void GetImages(int pageIndex)
{
    try
    {
        var imgs = (from fi in Files select fi.FullName).
                    IsImageFile().Skip(pageIndex * NumOfImageToFetch).
                    Take(NumOfImageToFetch);

        //NOTE : We could have also used the version of the IsImageFile() 
        //custom LINQ string extension method
        //that expects a predicate, something like
        //IsImageFile(f => f.StartsWith("png") || 
        //            f.StartsWith("jpg").Skip 

        fishPanel.Children.Clear();
        foreach (string filename in imgs)
        {
            if (UsingReflectiveImages.Value)
            {
                StoredImageControl si = new StoredImageControl
                {
                    OriginalFileUrl = filename,
                    Margin = new Thickness(5)
                };
                si.MouseDown += 
                  new System.Windows.Input.MouseButtonEventHandler(si_MouseDown);
                fishPanel.Children.Add(si);
            }
            else
            {
                StoredImage si = new StoredImage
                {
                    Source = new BitmapImage(new Uri(filename)),
                    Width = 100,
                    OriginalFileUrl = filename,
                    Margin = new Thickness(5)
                };
                si.MouseDown += new 
                   System.Windows.Input.MouseButtonEventHandler(si_MouseDown);
                fishPanel.Children.Add(si);
            }
        }
        btnPrev.IsEnabled = pageIndex > 0;
        btnNext.IsEnabled = 
          (Files.Length - (++pageIndex * NumOfImageToFetch)) >= 10;
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

The more eagle eyed among you may notice that there is a slightly curious bit of syntax, IsImageFile() used in the LINQ query. Mmm, how can that be? Well, the nice folk at Microsoft have now allowed us to create our own LINQ extensions, and that is exactly what this IsImageFile() thing is. Let us see this example:

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Data.Linq;
using System.Xml.Linq;
using System.Xml;

namespace MyFriends
{
    public static class CustomStringExtensions
    {
        public static IEnumerable<string> IsImageFile(
                      this IEnumerable<string> files, 
                      Predicate<string> isMatch)
        {
            foreach (string file in files)
            {
                if (isMatch(file))
                    yield return file;
            }
        }

        public static IEnumerable<string> 
               IsImageFile(this IEnumerable<string> files)
        {
            foreach (string file in files)
            {
                if (file.Contains(".jpg") || 
                        file.Contains(".png") || 
                        file.Contains(".bmp"))
                    yield return file;
            }
        }
    }
}

It can be seen that there are two methods for IsImageFile that both return IEnumerable<T> (string in this case), which is what LINQ extensions require an extension to provide. Another strange thing is that there is a this just shoved in the method signature; that bit of syntax allows the result of the query so far to be used in the new extension (this one effectively) being applied. The only other thing to note is that the type after the this keyword must match the current query result. So in the previous example, we are doing:

(from fi in Files select fi.FullName)

Which does indeed yield IEnumerable<string> so this extension IsImageFile may be used. Such as:

var imgs = (from fi in Files select fi.FullName).
           IsImageFile().Skip(pageIndex * NumOfImageToFetch).
           Take(NumOfImageToFetch);

XLINQ usage

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

  • When the Save Friend button on the AddNewFriendControl is clicked, a check is done to see if there is an XML in existence. If there isn't, a new XML file is created using XLINQ. A new Friend object is added to an internal collection of objects used by the data grid.
  • When the Save Friend button is clicked again, and there is no in memory held Friend objects, append to the XML file and add a new Friend object to the internal collection of objects used by the data grid. However, if there is no XML, just add a new Friend object to the internal collection of objects used by the data grid.
  • On application closure, store all the in memory held Friend objects to an XML file that will overwrite the existing XML file (if it exists).

That is the basic idea; write the XML file initially, bind the result of the file to an internal collection which the grid uses, and then maintain the internally held collection, and on exit, update the XML file on disk. Likewise, on load, read the XML file from disk into the memory held collection:

So shall we see some code? Well, the code for the AddNewFriendControl Save Friend button is as shown below:

private void btnSave_Click(object sender, RoutedEventArgs e)
{
    string xmlFilename = 
      (string)Application.Current.Properties["SavedDetailsFileName"];
    string fullXmlPath = 
      Path.Combine(Environment.CurrentDirectory, xmlFilename);

    bool allRequiredFieldsFilledIn = true;
    allRequiredFieldsFilledIn = IsEntryValid(txtFriendName) &&
                                IsEntryValid(txtEmail);
    allRequiredFieldsFilledIn = IsEmailValid(txtEmail.Text);

    if (allRequiredFieldsFilledIn)
    {
        if (File.Exists(fullXmlPath))
        {
            try
            {
                //if there is currently no XML and no Friends in
                //memory, append to file. This should never happen
                if (FriendsList.Instance().Count == 0)
                {
                    Friend friend = new Friend
                    {
                        ID = Guid.NewGuid(),
                        Name = friendContent.FriendName,
                        Email = friendContent.FriendEmail,
                        PhotoUrl = friendContent.PhotoUrl,
                        VideoUrl = friendContent.VideoUrl,
                        MusicUrl = friendContent.MusicUrl
                    };
                    XMLFileOperations.AppendToFile(fullXmlPath, friend);
                    FriendsList.Instance().Add(friend);
                    RaiseEvent(new RoutedEventArgs(FriendAddedEvent));
                    friendContent.Reset();
                    this.Reset();
                    MessageBox.Show("Sucessfully saved friend");
                }
                //otherwise simply update the singleton in memory
                //collection of friends, which will be written
                //to disk at closure of the application
                else
                {
                    FriendsList.Instance().Add(new Friend
                                {
                                    ID = Guid.NewGuid(),
                                    Name = friendContent.FriendName,
                                    Email = friendContent.FriendEmail,
                                    PhotoUrl = friendContent.PhotoUrl,
                                    VideoUrl = friendContent.VideoUrl,
                                    MusicUrl = friendContent.MusicUrl
                                });
                    RaiseEvent(new RoutedEventArgs(FriendAddedEvent));
                    friendContent.Reset();
                    this.Reset();
                    MessageBox.Show("Sucessfully saved friend");
                }
            }
            catch
            {
                MessageBox.Show("Error updating friends details");
            }
        }
        else
        {
            try
            {
                Friend friend = new Friend
                {
                    ID = Guid.NewGuid(),
                    Name = friendContent.FriendName,
                    Email = friendContent.FriendEmail,
                    PhotoUrl = friendContent.PhotoUrl,
                    VideoUrl = friendContent.VideoUrl,
                    MusicUrl = friendContent.MusicUrl
                };
                XMLFileOperations.CreateInitialFile(fullXmlPath, friend);
                FriendsList.Instance().Add(friend);
                RaiseEvent(new RoutedEventArgs(FriendAddedEvent));
                friendContent.Reset();
                this.Reset();
                MessageBox.Show("Sucessfully saved friend");
            }
            catch(Exception ex)
            {
                MessageBox.Show("Error saving friends details");
            }
        }
    }
    else
    {
        MessageBox.Show("You need to either fill in one of the fields, " + 
         "or correct it", "Error", MessageBoxButton.OK,
         MessageBoxImage.Error);
    }
}

And recall, earlier I mentioned that there was a second singleton that was used by the ViewAllUsersControl to allow it to maintain the correct data to show in the data grid. Well, that's the FriendsList singleton shown below:

using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Text;
using System.Windows;

namespace MyFriends
{
    public class FriendsList : ObservableCollection<Friend>
    {

        private static FriendsList instance;

        private FriendsList()
        {
            try
            {
                XMLFileOperations.XmlFilename = 
                    (string)Application.Current.Properties["SavedDetailsFileName"];

                List<Friend> theList = XMLFileOperations.GetFriends();
                foreach (Friend friend in theList)
                {
                    this.Add(friend);
                }
            }
            catch { }
        }

        public static FriendsList Instance()
        {
            if (instance == null)
            {
                instance = new FriendsList();
            }
            return instance;
        }
    }
}

But wait, this also calls yet another class to get its data, so the story isn't finished yet. Let's follow this path. There is another class called XMLFileOperations. Let's see the method in that class.

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Data.Linq;
using System.Xml.Linq;
using System.Xml;
using System.IO;

namespace MyFriends
{
    public class XMLFileOperations
    {
        public static string XmlFilename { get; set; }

        public static List<Friend> GetFriends()
        {
            string fullXmlPath = System.IO.Path.Combine(Environment.CurrentDirectory, 
                                                        XMLFileOperations.XmlFilename);

            var xmlFriendResults =
                from friend in StreamElements(fullXmlPath, "Friend")
                select new Friend
                {
                    ID = new Guid(friend.Element("ID").Value),
                    Name = friend.Element("name").SafeValue(),
                    Email = friend.Element("email").SafeValue(),
                    PhotoUrl = friend.Element("photo").SafeValue(),
                    VideoUrl = friend.Element("video").SafeValue(),
                    MusicUrl = friend.Element("music").SafeValue()
                };

            return xmlFriendResults.ToList();
        }

        //*************************************************************
        //    NOTE : THIS IS HOW YOU WOULD LOAD THE DOCUMENT IN ONE GO
        //           BUT IF YOU HAVE A LARGE XML DOCUMENT THE LOAD(..)
        //           METHOD MIGHT BE A BOTTLE NECK
        //*************************************************************

        //public static IEnumerable<XElement> GetFriendItems(string uri)
        //{
        //        var xmlDoc = XDocument.Load(uri);
        //        var xmlElement =
        //            xmlDoc.Root.Element("MyFriends").Elements("Friend");
        //        foreach (var xmlElement in xmlElement)
        //            yield return xmlElement;
        //}

        public static void CreateInitialFile(string fullXmlPath, Friend friend)
        {
            XElement friendsXmlDocument =
                new XElement("MyFriends",
                    new XElement("Friend",
                        new XElement("ID", friend.ID),
                        new XElement("name", friend.Name),
                        new XElement("email", friend.Email),
                        new XElement("photo", friend.PhotoUrl),
                        new XElement("video", friend.VideoUrl),
                        new XElement("music", friend.MusicUrl))
                );
            friendsXmlDocument.Save(fullXmlPath);
        }

        public static void AppendToFile(string fullXmlPath, Friend friend)
        {
            XElement friendsXmlDocument = XElement.Load(fullXmlPath);
                friendsXmlDocument.Add(new XElement("Friend",
                new XElement("ID", friend.ID),
                new XElement("name", friend.Name),
                new XElement("email", friend.Email),
                new XElement("photo", friend.PhotoUrl),
                new XElement("video", friend.VideoUrl),
                new XElement("music", friend.MusicUrl))
            );
            friendsXmlDocument.Save(fullXmlPath);
        }

        public static IEnumerable<XElement> StreamElements(string uri, string name)
        {
            using (XmlReader reader = XmlReader.Create(uri))
            {
                reader.MoveToContent();
                while (reader.Read())
                {
                    if ((reader.NodeType == XmlNodeType.Element) &&
                      (reader.Name == name))
                    {
                        XElement element = (XElement)XElement.ReadFrom(reader);
                        yield return element;
                    }
                }
                reader.Close();
            }
        }

        public static void SaveOnExit()
        {
            string xmlFilename = (string)
               System.Windows.Application.Current.Properties["SavedDetailsFileName"];
            string fullXmlPath = Path.Combine((string)
              System.Windows.Application.Current.Properties["SaveFolder"], 
              xmlFilename);
            XDocument document = new XDocument(
               new XElement("MyFriends", getExistingElements()));
            document.Save(fullXmlPath);

        }

        private static List<XElement> getExistingElements()
        {
            List<XElement> elements = new List<XElement>();
            foreach (Friend friend in FriendsList.Instance())
            {
                elements.Add(new XElement("Friend",
                        new XElement("ID", friend.ID),
                        new XElement("name", friend.Name),
                        new XElement("email", friend.Email),
                        new XElement("photo", friend.PhotoUrl),
                        new XElement("video", friend.VideoUrl),
                        new XElement("music", friend.MusicUrl)));
            }
            return elements;
        }
    }
}

Most of this little lot is standard XLINQ, with the exception of one little method; the StreamElements() is special. And why is it special? Well, it allows us to return a single IEnumerable<XElement> to a standard XLINQ query one element at a time. Which will help for large XML files where loading time may be a consideration. It uses the fairly new C# yield keyword.

Also of note in this little lot is yet another custom LINQ extension, the SafeValue XLINQ extension that targets XElement objects. This XLINQ extension is shown below:

using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Data.Linq;
using System.Xml.Linq;
using System.Xml;

namespace MyFriends
{
    public static class CustomXElementExtensions
    {
        public static string SafeValue(this XElement input)
        {
            return (input == null) ? string.Empty : (string)input.Value;
        }
    }
}

So with this in place, we are able to write things like:

friend.Element("name").SafeValue()

And our nice little SafeValue() XLINQ extension ensures we will get a nice value back instead of null.

I hope you are seeing the value that LINQ will bring to us as developers. I think, used carefully, it's well cool and powerful.

Getting gushy about the Xceed data grid

Like I said right at the beginning of this article, I would not normally include a third party product unless I thought it was of some use to me or you for that matter. And I have to say, the free Xceed datagrid for WPF is just awesome. You can basically do the following:

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

In fact, you can totally restyle it, if that's your bag. Me, I just wanted to try it out for size. Basically, I've tried all the things above, so I'll talk about each of them in turn.

The first thing to note is that the Xceed data grid is hosted within the ViewAllUsersControl control. As such, all the relevant mark up is within the ViewAllUsersControl.xaml file. Another thing to note is that the Xceed data grid is bound to the results of the FriendsList object, which is an ObservableCollection<Friend> type of object. But we still haven't seen the Friend object, have we? Probably best to have a quick look at that, so we can see where the grid bindings are coming from.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyFriends
{
    public class Friend
    {
        public Guid ID { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
        public string PhotoUrl { get; set; }
        public string VideoUrl { get; set; }
        public string MusicUrl { get; set; }
    }
}

The bound grid looks something like this:

That's simple, isn't it? Anyway, let's crack on.

Create cell templates

This is a synch; all that you have to do is create a column and define a template for it. An example of this is shown below for the ImageUrl bound column:

<!-- Photo Column-->
<xcdg:Column FieldName="PhotoUrl" VisiblePosition="3" Visible="True">
    <!-- Content Non-Edit mode -->
    <xcdg:Column.CellContentTemplate>
        <DataTemplate>
            <StackPanel Margin="5,5,5,5" 
                       VerticalAlignment="Center" 
                       HorizontalAlignment="Left">
                <Border BorderBrush="White" BorderThickness="2" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center">
                    <Image x:Name="img" 
                           Source="{Binding}" Stretch="Fill" 
                           Width="46" Height="46">
                        <Image.ToolTip>
                                <Border BorderBrush="White" 
                                        BorderThickness="6" 
                                        HorizontalAlignment="Center" 
                                        VerticalAlignment="Center">
                                    <Image Source="{Binding}" 
                                       Width="150" Height="150" 
                                       Stretch="Fill" 
                                       x:Name="imgTool"></Image>
                                </Border>
                        </Image.ToolTip>   
                    </Image>
                </Border>
                <Border Width="50" Height="50" 
                       BorderBrush="White" BorderThickness="2" 
                       HorizontalAlignment="Center" >
                    <Border.Background>
                        <VisualBrush Visual="{Binding ElementName=img}">
                            <VisualBrush.Transform>
                                <ScaleTransform ScaleX="1" ScaleY="-1" 
                                      CenterX="50" 
                                      CenterY="25"></ScaleTransform>
                            </VisualBrush.Transform>
                        </VisualBrush>
                    </Border.Background>
                    <Border.OpacityMask>
                        <LinearGradientBrush 
                                StartPoint="0,0" EndPoint="0,1">
                            <GradientStop Offset="0" 
                                Color="Black"></GradientStop>
                            <GradientStop Offset="0.6" 
                                Color="Transparent"></GradientStop>
                        </LinearGradientBrush>
                    </Border.OpacityMask>
                </Border>
            </StackPanel>
        </DataTemplate>
    </xcdg:Column.CellContentTemplate>

As the Xceed datagrid allows us to create our own Templates, we can simply create whatever content we want for a particular cell, and that's what I've done. In this example, we have an image being shown in a cell using a reflection. And this looks like the cell below:

Create cell editor templates

This is actually pretty easy too, as Xceed has made that pretty easy by just creating another type of Template, an EditTemplate. Let's see one of these:

<xcdg:Column.CellEditor>
    <xcdg:CellEditor>
        <xcdg:CellEditor.EditTemplate>
            <DataTemplate>
                <StackPanel  Margin="0,0,0,0" Orientation="Vertical" 
                       Background="{StaticResource blackLinearBrush}"
                       VerticalAlignment="Center" HorizontalAlignment="Left">
                    <Border BorderBrush="White" BorderThickness="2" 
                            HorizontalAlignment="Center" 
                            VerticalAlignment="Center">
                        <Image x:Name="imgNew" 
                               Source="{xcdgWeb:CellEditorBinding}" 
                               Stretch="Fill" 
                               Width="46" Height="46">
                            <Image.ToolTip>
                                <Border BorderBrush="White" 
                                        BorderThickness="6" 
                                        HorizontalAlignment="Center" 
                                        VerticalAlignment="Center">
                                    <Image Source="{Binding}" Width="150" 
                                         Height="150" 
                                         Stretch="Fill"></Image>
                                </Border>
                            </Image.ToolTip>
                        </Image>
                    </Border>
                    <Button x:Name="btnAssignNewImage" Content="`" 
                       Template="{DynamicResource GlassButton}" 
                       FontFamily="Webdings" FontSize="15" 
                       FontWeight="Normal" Foreground="#FFFFFFFF" 
                       ToolTip="Assign New Image" Width="50" 
                       Height="25" Margin="0,5,0,0" 
                       HorizontalAlignment="Center" 
                       Click="btnAssignNewImage_Click"/>
                </StackPanel>
            </DataTemplate>
        </xcdg:CellEditor.EditTemplate>
    </xcdg:CellEditor>
</xcdg:Column.CellEditor>

Of course, in this case, as we are actually applying a new value, we need some code-behind functionality to do the edit, such as:

private void btnAssignNewImage_Click(object sender, RoutedEventArgs e)
{
    Point topleft = this.PointToScreen(new Point(0, 0));
    DisplayStyle newDisplayStle = 
      (DisplayStyle)Application.Current.Properties["SelectedDisplayStyle"];
    double heightOffset = newDisplayStle == DisplayStyle.ThreeDimension ? 20 : 0;

    AddFriendImageWindow addImageWindow = new AddFriendImageWindow();
    (addImageWindow as Window).Height = this.Height + heightOffset;
    (addImageWindow as Window).Width = this.Width;
    (addImageWindow as Window).Left = topleft.X;
    (addImageWindow as Window).Top = topleft.Y;
    addImageWindow.ShowDialog();
    if (!string.IsNullOrEmpty(addImageWindow.SelectedImagePath))
    {
        StackPanel panel = 
          VisualTreeHelper.GetParent(sender as DependencyObject) as StackPanel;
        Image image = panel.FindName("imgNew") as Image;
        if (image != null)
        {
            image.Source = 
              new BitmapImage(new Uri(addImageWindow.SelectedImagePath));
        }
    }
}

Still fairly OK, me thinks. Not so painful, is it?

Create cell validators

Another thing I personally like with this grid is that it is possible to plug in a validator into certain cells. Let's see an example of this in action.

So we define a cell to have validation like this:

<xcdg:Column FieldName="Email" VisiblePosition="2" 
          CellErrorStyle="{StaticResource cell_error}" Visible="True">
    <xcdg:Column.CellValidationRules>
        <local:EmailValidationRule/>
    </xcdg:Column.CellValidationRules>
</xcdg:Column>

And then we have a validator like:

using System;
using System.Collections.Generic;
using System.Text;
using Xceed.Wpf.DataGrid;
using Xceed.Wpf.DataGrid.ValidationRules;
using System.Windows.Controls;
using System.Globalization;
using System.Text.RegularExpressions;

namespace MyFriends
{

    public class EmailValidationRule : CellValidationRule
    {
        public override ValidationResult 
               Validate(object value, CultureInfo cultureInfo,
                        CellValidationContext cellValidationContext)
        {
            string pattern = 
              @"^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$";
            Regex regEx=new Regex(pattern);
            if (!regEx.IsMatch((string)value))
            {
                return new ValidationResult(false, 
                       "You entered an invalid email");
            }
            return new ValidationResult(true, null);
        }
    }
}

And then at runtime, we have a validator in action:

Create a different view

The Xceed grid supports several different views straight out of the box, such as a table and card. I have provided two buttons to toggle between these views. The switching between these views is easily achieved as follows:

private void btnTableView_Click(object sender, RoutedEventArgs e)
{
    TableView tv = new TableView();
    tv.Theme = new LunaMetallicTheme();
    dgFriends.View = tv;
}

private void btnCardView_Click(object sender, RoutedEventArgs e)
{
    CardView cv = new CardView();
    cv.Theme = new LunaMetallicTheme();
    dgFriends.View = cv;
}

And to see what the data grid looks like in card view:

All in all, I am very, very impressed by the Xceed grid, and the fact that it's free should not be overlooked. I'll be using it on a real project if my requirements need some sort of tabular data.

Some slightly sneaky value converters

As part of this application, I wanted to be able to hide a certain element based on whether another element's source was empty or not. To this end, I crafted a ValueConverter that does this trick, which is as shown below. This is used within the Xceed data grid cell templates to ensure that for both the video and music cells, the play/stop/view buttons are only shown if the associated MediaElement's source is not empty.

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media.Imaging;

namespace MyFriends
{
    [ValueConversion(typeof(Uri), typeof(Visibility))]
    public class SourceToVisibilityConverter : IValueConverter
    {
        #region Instance Fields
        public static SourceToVisibilityConverter Instance = 
                      new SourceToVisibilityConverter();
        #endregion
        #region IValueConverter implementation
        public object Convert(object value, Type targetType, 
                              object parameter, CultureInfo culture)
        {
            try
            {
                return (value as Uri).AbsolutePath.Equals(string.Empty) ? 
                                      Visibility.Collapsed : Visibility.Visible;
            }
            catch
            {
                return Visibility.Collapsed;
            }
        }

        public object ConvertBack(object value, Type targetType, 
                                  object parameter, CultureInfo culture)
        {
            throw new NotSupportedException("Cannot convert back");
        }
        #endregion
    }
}

So we can use this converter as follows in the XAML:

Visibility="{Binding Path=Source, ElementName=videoSrc, 
            Mode=Default, 
            Converter={x:Static local:SourceToVisibilityConverter.Instance}}"

Vista style dialogs

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

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

namespace MyFriends
{
    /// <summary>
    /// One item in the common dialog filter.
    /// </summary>

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

        public string Display
        {
            get { return display; }
        }

        public string Extention
        {
            get { return extention; }
        }

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

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

    class CommonDialog
    {
        #region fields

        // Structure used when displaying Open and SaveAs dialogs.

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

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

        #region properties

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

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

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

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

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

        #endregion

        #region pinvoke details

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

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

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

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

        #endregion

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

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

        /// <summary>
        /// Display the Vista-style common Open dialog.
        /// </summary>

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

        /// <summary>
        /// Display the Vista-style common Save As dialog.
        /// </summary>

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

        /// <summary>
        /// Set the low level filter with the filter collection.
        /// </summary>

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

References

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

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)

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
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 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • 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

Comments and Discussions

 
GeneralMy vote of 3 PinmemberPer Hyyrynen9-Feb-12 20:57 
QuestionGive demo application PinmemberJavedsamrt22-Dec-11 2:42 
AnswerRe: Give demo application PinmvpSacha Barber22-Dec-11 4:27 
GeneralRe: Give demo application PinmemberJavedsamrt25-Dec-11 20:08 
QuestionERROR: Could not resolve this reference PinmemberJavedsamrt8-Dec-11 22:34 
AnswerRe: ERROR: Could not resolve this reference PinmvpSacha Barber9-Dec-11 0:10 
GeneralRe: ERROR: Could not resolve this reference PinmemberJavedsamrt12-Dec-11 21:05 
GeneralMy vote of 5 Pinmemberahsan sarfraz24-Jan-11 1:29 
GeneralNice PinmemberNinjaCross15-Nov-10 22:23 
GeneralRe: Nice PinmvpSacha Barber15-Nov-10 22:43 
GeneralRe: Nice PinmemberNinjaCross15-Nov-10 22:51 
GeneralRe: Nice PinmvpSacha Barber15-Nov-10 23:24 
GeneralAwesome Pinmemberpranav9516-Jun-10 2:17 
GeneralRe: Awesome PinmvpSacha Barber16-Jun-10 2:41 
QuestionInformation about Presentation Framework Pinmemberwebroy200023-Feb-10 12:27 
AnswerRe: Information about Presentation Framework PinmvpSacha Barber23-Feb-10 21:41 
AnswerRe: Information about Presentation Framework PinmvpSacha Barber16-Mar-10 23:29 
QuestionNeed Help here. PinmemberJayze Joves16-Aug-09 16:09 
AnswerRe: Need Help here. PinmvpSacha Barber16-Aug-09 21:43 
JokeOrange PinmemberWillemToerien6-Jul-09 3:42 
GeneralRe: Orange PinmvpSacha Barber6-Jul-09 5:29 
Generalso cOOl PinmemberMember 44981699-Mar-09 21:01 
GeneralRe: so cOOl PinmvpSacha Barber9-Mar-09 22:39 
Questionhow can i invoke the flipping panel from codebehind? Pinmemberchristoph braendle1-Dec-08 13:12 
GeneralXceed Grid Pinmemberclosl26-Sep-08 12:38 
GeneralRe: Xceed Grid PinmvpSacha Barber26-Sep-08 20:36 
GeneralRe: Xceed Grid Pinmembermsurma10-May-10 12:35 
GeneralRe: Xceed Grid PinmvpSacha Barber10-May-10 19:54 
QuestionI have a problem deleting Users Pinmemberzuare25-May-08 23:04 
AnswerRe: I have a problem deleting Users PinmvpSacha Barber26-May-08 10:06 
GeneralYou rock Pinmemberamiah26-Apr-08 10:49 
GeneralRe: You rock PinmvpSacha Barber28-Apr-08 1:37 
Generalhelp Pinmemberdjsoul12-Mar-08 1:50 
GeneralRe: help PinmvpSacha Barber12-Mar-08 2:03 
GeneralRe: help Pinmemberdjsoul12-Mar-08 9:34 
GeneralRe: help PinmvpSacha Barber12-Mar-08 10:33 
GeneralRe: help Pinmemberdjsoul12-Mar-08 10:47 
GeneralRe: help PinmvpSacha Barber13-Mar-08 0:52 
GeneralRe: help Pinmemberdjsoul13-Mar-08 2:29 
GeneralRe: help PinmvpSacha Barber13-Mar-08 2:40 
GeneralRe: help Pinmemberdjsoul13-Mar-08 5:49 
GeneralRe: help PinmvpSacha Barber13-Mar-08 6:32 
GeneralNice Article PinmemberDreamzor4-Feb-08 23:15 
GeneralRe: Nice Article PinmvpSacha Barber4-Feb-08 23:50 
Generalreally good article. PinmemberKennetho3-Feb-08 21:32 
GeneralRe: really good article. PinmvpSacha Barber3-Feb-08 22:07 
GeneralCool PinmemberMazai20-Dec-07 6:37 
GeneralRe: Cool Pinmember Dr.Luiji 24-Jan-08 4:34 
GeneralRe: Cool PinmvpSacha Barber24-Jan-08 4:47 
QuestionViewport2DVisual3D? PinmemberJared Bienz [MSFT]14-Dec-07 4:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.141029.1 | Last Updated 18 Jan 2008
Article Copyright 2007 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid