In This Chapter
Controls are a convenient way to “pack” functionality and to provide encapsulation, reusability, and testability. When Silverlight was first released, the only control delivered was a TextBlock
. Needless to say, it was not easy to create rich applications! Of course, you also had a set of “primitives” such as Rectangles
, Ellipses
, and even Paths
, not to mention the powerful MediaElement
. But the point remains, building an application without even a Button
or a Slider
is tough. Some (such as the team building the video player templates for Expression Encoder) created buttons and other controls from scratch (you can still see this if you open one of the player templates in Blend, as we did in Chapter 13, “Progressing with Videos”).
Fortunately, we now have a rich set of controls. Even better, we have a framework that allows us to create new controls and also to customize existing controls’ look and feel. This is liberating. However, creating controls is not easy—it’s advanced Silverlight! This chapter and the next help you understand how such a complex task is done, and encourage you to continue the exploration.
A UserControl
(as opposed to a custom control, which we create later) has a XAML front end in addition to the code-behind. In this section we customize the control in which our thumbnails are displayed and scrolled. To do this, we create a new UserControl
composed of a ScrollViewer
(without any scrollbars visible), an ItemsControl
, and two RepeatButtons
. We talked about ItemsControls
in Chapter 18, “Data Binding and Using Data Controls,” and saw how they can be used to display a collection of items. This enables us to move toward a model where the media items are totally external to the Thumbnails application (for example, placed on the web server).
The next step is use an XML file to describe the items, and to finally be free from any hard-coded value. Instead, we “feed” an XML file to our application, and the media files are loaded dynamically from the web server.
Our application and its extended functionality require some code-behind. Since we are experienced object-oriented programmers now, let’s “break” our functionality in objects.
Media
We want a new class to hold our thumbnails information. This is the class that we will “fill” with data, add to a collection, and then data bind to our thumbnails viewer (that’s the control we do next). Follow the steps:
1. In Visual Studio, open the Thumbnails application that we edited last in Chapter 17, “Using Resources, Styling, and Templating.” Then right-click on the Thumbnails Solution (the first file on top of the Solution Explorer) and add a new project to the Thumbnails Solution. This should be a Silverlight class library named MediaInfo.
2. Rename the class file in Solution Explorer from Class1.cs to Media.cs.
3. Replace the Media class with the code in Listing 19.1:
public enum MediaType
{
Image = 0,
Movie = 1,
}
public class Media : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
public MediaType Type
{
get; set;
}
public string MediaName
{
get; set;
}
private string _description;
public string Description
{
get { return _description; }
set
{
_description = value;
if (PropertyChanged != null)
{
PropertyChanged(this,
new PropertyChangedEventArgs(″Description″));
}
}
}
private string _mediaPath = ″″;
public string MediaPath
{
get { return _mediaPath; }
set { _mediaPath = (value == null) ? ″″ : value; }
}
public DateTime TimeStamp
{
get; set;
}
}
First we add an enumeration named MediaType
that we will use to define the type of the current media.
The Media
class has five properties. Of these five, only the Description
might be updated at runtime, so it’s the only one that raises the PropertyChanged
event.
The property MediaPath
may never be null
, because it might be combined to the MediaName
to form the full path of the media item. We check whether it’s set to null
, and we set it to an empty string instead.
Build the application now to make sure that everything is OK. You will have to add a using directive for the INotifyPropertyChanged
interface.
Media
ClassThe Media
class is an independent data object. When we build our Thumbnail template later, we will need a couple more properties, directly related to how we display our media elements. The best way to do this is to extend the Media
class. Because this new class is extended, we name it MediaEx
. Also, because it’s really only used in our Thumbnails application, we create it in that project with the following steps:
1. Right-click on the Data folder in the Thumbnails project in Studio and select Add, Class. Name the new class MediaEx.cs.
2. We need a reference to the external DLL MediaInfo. We add it through a project reference. Right-click on the Thumbnails project again and select Add Reference. In the Add Reference dialog, choose the Projects tab and select the MediaInfo project.
3. The class looks like Listing 19.2:
public class MediaEx : Media
{
public Uri FileUri
{
get
{
return new Uri(
System.IO.Path.Combine(MediaPath, MediaName).Replace(′\′, ′/′),
UriKind.Relative);
}
}
public BitmapImage ImageSource
{
get
{
return (Type == MediaType.Image)
? new BitmapImage(FileUri) : null;
}
}
public Uri MovieUri
{
get { return (Type == MediaType.Movie)
? FileUri : null; }
}
}
None of these properties change during runtime, so they don’t raise the PropertyChanged
event.
The FileUri
property is just a convenience property, but since it’s read-only, it does no harm to make it public. Note the use of the helper method System.IO.
Path
.Combine
. Unfortunately, this method uses the backslash separator (′′
) and not the slash (′/′
), which is needed for URIs. This is why we need to replace the ′′
character with ′/′
.
The ImageSource
(of type BitmapImage
) and MovieUri
(of type Uri
) are what we need to bind to an Image
control, and respectively, a MediaElement
control. If the Type
doesn’t correspond, they return null
and nothing appears on the UI.
4. We want the MediaEx
items stored in an ObservableCollection
. Since the data source class is very simple, you can write the code in Listing 19.3 in the same file MediaEx.cs. If you prefer, however, you can also create a new class file called MediaExCollection.cs in the Data folder. At this point, you can try and build the application. It should build fine, after you add a couple of using
directives.
public class MediaExCollection : ObservableCollection<MediaEx>
{
}
ThumbnailsViewerControl
With the data objects ready, let’s work on the user interface now with the following steps:
1. Build the application in Visual Studio, and then open it in Expression Blend. If you see an error message, just recompile the application in Blend.
2. In Page.xaml, in the Objects and Timeline category, delete the ScrollViewer
containing the Thumbnails.
3. Add a new Grid
to the LayoutRoot, and name it ThumbnailsViewer. Move the new Grid
in the Objects and Timeline category until it appears before the StackPanel
, just under the LayoutRoot.
4. Set the ThumbnailsViewer Grid
’s Width
to 170 pixels and reset its Height
to Auto. Reset the HorizontalAlignment
and VerticalAlignment
to Stretch and reset the Margin
to 0. Set the Column
to 1 and the RowSpan
to 2 so that it appears in the same cells as the previous ScrollViewer
.
5. Select the new Grid
until it appears surrounded with a yellow border. Then, with the Selection tool selected in the toolbar, create three rows. The first and the last are going to “host” buttons to scroll the content. Make them 75 pixels high. The middle row will “host” the Thumbnails, and its Height
should be set to 1 Star.
6. Open the Asset Library by clicking on the last button on the toolbar.
7. Check the Show All box and select a RepeatButton
. This control gets added as a tool to the toolbar, right above the Asset Library button.
8. Using the RepeatButton
tool, add a button to the first cell of the ThumbnailsViewer Grid
and make it fill the whole cell. Name this button ScrollButtonUp.
9. Repeat the operation in the last cell on the right, and name this new button ScrollButtonDown.
10. In the middle cell, add a ScrollViewer
and make it fill the whole cell. Name it ThumbScrollViewer. Set its HorizontalScrollbarVisibility
to Disabled and VerticalScrollBarVisibility
to Hidden.
11. Select the ScrollViewer
with a yellow border and add an ItemsControl
to it. Here too, this control must be selected from the Asset library (like the RepeatButton
before). And here, too, this control should fill the whole ScrollViewer
. Name it ThumbItemsControl. The result should look like Figure 19.1.
Figure 19.1 ThumbnailsViewer
control draft
12. Finally, we “pack” our UserControl
. This gives us more flexibility to work with it and provides neater code. Right-click on the ThumbnailsViewer Grid
in the Objects and Timeline category. Select Make Control (or press the F8 key).
13. Enter a name for the new control: ThumbnailsViewerControl (the name proposed by Blend) sounds all right. Do not check the Leave Original Content As Is And Create Duplicates As Necessary box. Click OK.
Blend creates a new UserControl
and places the ThumbnailsViewer Grid
within the new control’s LayoutRoot Grid
. That’s not really necessary, since ThumbnailsViewer is a grid already. We will simply replace LayoutRoot with ThumbnailsViewer as shown in the following steps:
1. Select ThumbnailsViewer in the Objects and Timeline category, and drag it on the UserControl
on top. Since the UserControl
can have only one child, the LayoutRoot disappears.
2. Rename ThumbnailsViewer to LayoutRoot. Although not required, it’s good practice to name the top container like this.
3. In the Properties panel, set the Width
and Height
of LayoutRoot to Auto. When you do this, even though all the elements’ Width
and Height
are set to Auto, Blend sets a “design width and height” to help you work. If it didn’t, our control would appear very small.
4. Save everything and go back to the Page.xaml.
5. In Page.xaml, set the ThumbnailsViewerControl
’s Width
to 170 pixels if needed. Reset the Margin
to 0. Also make sure that the Column
in which the ThumbnailsViewer is located is 1 and the RowSpan
is 2!
6. Notice that the Design panel asks you to rebuild the project, which you should do now.
7. Finally, set the ThumbnailsViewerControl
Name to ThumbnailsViewer. You can now run the application to see the result.
Our UserControl
should get some information from the Page
, and also expose information when an item is selected.
ItemsSource
PropertyWe need to provide the Page
with a way to set the items that must be displayed. To do this, let’s add a DependencyProperty
that we will use as a gateway to set our ItemsControl
’s ItemsSource
property. We reuse the same name to make our intent clear.
We mentioned DependencyProperties
in Chapter 15, “Digging into Silverlight Elements,” and said that they are the mortar of Silverlight, holding pieces together.
DPs can be added to a DependencyObject
(and remember that FrameworkElement
derives from DependencyObject
). A DP can be animated, it can be styled, and two DPs can be data bound together (as we saw in previous chapters). Generally, any property that could change during the life of the control, and any property that you want to data bind to should probably be a DP.
Actually, Windows Presentation Foundation uses DPs even more than Silverlight (and WPF DPs are also more powerful than Silverlight’s one because WPF is a superset of Silverlight). In Silverlight, you cannot derive a class from DependencyObject
, so you cannot use DPs with data items, but only with controls.
DependencyProperties
are registered with the Silverlight framework. The registration process occurs once only for every class (it’s a static registration), but every instance of the class gets a separate set of DPs anyway. Let’s add a DP to our control with the following steps:
1. Open the file ThumbnailsViewerControl.xaml.cs in Visual Studio. You will have to reload the project there, as we changed it in Blend.
2. Add an event handler inside the ThumbnailsViewerControl
class as in Listing 19.4. It will be called when the ItemsSource
DP is set. Whenever this happens, we want to use the value to set the ItemsControl
ItemsSource
.
private static void OnItemsSourceChanged(DependencyObject d,
DependencyPropertyChangedEventArgs args)
{
ThumbnailsViewerControl sender = d as ThumbnailsViewerControl;
sender.ThumbItemsControl.ItemsSource
= (args.NewValue as MediaExCollection);
}
Because this is a static event handler (remember that DPs are registered in a static way with the framework, so the event handler must be static too), we need to get a reference on the instance that sent the event. This is what the first parameter does. On the first line, we cast this object d
to a ThumbnailsViewerControl
.
Note how we get the NewValue
of the MediaExCollection
instance in the args
parameter of OnMediaChanged
. You also get the OldValue
if you need it.
3. Add the DP as shown in Listing 19.5:
1 public static readonly DependencyProperty ItemsSourceProperty
2 = DependencyProperty.Register(″ItemsSource″,
3 typeof(MediaExCollection), typeof(ThumbnailsViewerControl
4 new PropertyMetadata(new PropertyChangedCallback(OnItemsSourceChanged)));
On line 1, we declare the name of the DP.
Line 2 is the call to the static method Register
on the DependencyProperty
class. The first parameter is the name of the property.
Line 3 states the type of the DP, and then the type of the owner.
Line 4 creates a new PropertyMetadata
instance. This type contains additional information about the DP. In our case, we “wire” the event handler we implemented before, through a PropertyChangedCallback
handler. This guarantees that our event handler is called every time the DP’s value changes.
4. For convenience, add “normal” get and set accessors for the DP as in Listing 19.6. This makes it much easier to work with it, for example, to set the DP in code.
public MediaExCollection ItemsSource
{
get { return (MediaExCollection) GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
The DP’s value for each instance can be read by a call to the method GetValue
. To write, use the method SetValue
. Build the code to make sure that everything is in order. You can build the application now.
SelectedItem DependencyProperty
Add the code as shown in Listing 19.7:
public MediaEx SelectedItem
{
get { return (MediaEx) GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty
= DependencyProperty.Register(″SelectedItem″,
typeof(MediaEx), typeof(ThumbnailsViewerControl),
new PropertyMetadata(null));
We set this DP later in this chapter, when we handle the MouseLeftButtonDown
event on each Thumbnail.
To trigger the “expanding” and “collapsing” animations in the Page
, we expose an event that will be raised when a Thumbnail is clicked. Here, too, we can inspire ourselves from the ListBox
and its SelectionChanged
event. We will in fact reuse the same delegate and the SelectionChangedEventArgs
class for our purpose.
1. In the ThumbnailsViewerControl
class, declare an event:
public event SelectionChangedEventHandler SelectionChanged;
2. Create a helper method to raise this event (Listing 19.8):
private void OnSelectionChanged(FrameworkElement sender, MediaEx item)
{
if (SelectionChanged != null)
{
object[] addedItems = new object[1];
addedItems[0] = sender;
object[] removedItems = new object[0];
SelectionChangedEventArgs args
= new SelectionChangedEventArgs(removedItems, addedItems);
SelectionChanged(this, args);
}
SelectedItem = item;
}
For more information, check the class SelectionChangedEventArgs
in the SDK documentation.
In the last line of the method set the SelectedItem
property to the MediaEx
instance currently selected. If any other property of another control is bound to that DP, it is notified of the change. We see how this works in Chapter 20.
At this point, you can build the application again to make sure that everything is OK. You can even run it, though you won’t notice any changes, we still need to update the UI!
DataTemplate
Our UserControl
’s functionality is ready. Now we need to create items to fill it, and a DataTemplate
for these items. We also need to trigger the SelectionChangedEvent
and set the SelectedItem
correctly.
Since we don’t know how to read an XML file yet (we learn this in Chapter 22, “Connecting to the Web”), we now create the Media
instances in XAML markup. This is temporary. Follow the steps:
1. Open the file Page.xaml in Visual Studio.
2. Add a namespace in the UserControl
tag pointing to the namespace Thumbnails.Data:
xmlns:data=″clr-namespace:Thumbnails.Data″
3. Because we use the items in Blend, we want to notify Blend that this is a data source. To do this, we need the namespaces in Listing 19.9 added to the UserControl
tag (we talked about this in Chapter 18). Note however that it is very possible that Blend already added these namespaces. Make sure that they are not already available before you add them, or else you’ll get an error.
xmlns:d=″http://schemas.microsoft.com/expression/blend/2008″
xmlns:mc=″http://schemas.openxmlformats.org/markup-compatibility/2006″
mc:Ignorable=″d″
4. In the UserControl.Resources
section, add the markup in Listing 19.10:
<data:MediaExCollection x:Key=″MediaDataSource″ d:IsDataSource=″True″>
<data:MediaEx MediaName=″mov1.wmv″ MediaPath=″MediaFiles″ Type=″Movie″
Description=″Nightly show at Singapore Zoo″ />
<data:MediaEx MediaName=″pic1.png″ MediaPath=″MediaFiles″ Type=″Image″
Description=″The Matterhorn seen from Zermatt″ />
<data:MediaEx MediaName=″pic2.jpg″ MediaPath=″MediaFiles″ Type=″Image″
Description=″The Matterhorn″ />
<data:MediaEx MediaName=″pic3.jpg″ MediaPath=″MediaFiles″ Type=″Image″
Description=″Mountains seen from Klosters″ />
</data:MediaExCollection>
This markup creates four instances of the MediaEx
class and initializes them with information about the files. The path MediaFiles
is the name of a (yet to be created) folder in which we will copy the media files. If you want, you can create a more complex file structure.
The four instances are added to an instance of the MediaExCollection
class we created before. Remember that this class inherits ObservableCollection
.
We set the d
:
IsDataSource
attribute to True
. We talked about this Expression Blend attribute in Chapter 18.
To help us design the DataTemplate
, we also add such a collection with test data in the ThumbnailsViewerControl.xaml file. We want test data there because it helps us visualize the items when we design them in Blend.
1. Open ThumbnailsViewerControl.xaml and set the data
namespace in the UserControl
tag.
xmlns:data=″clr-namespace:Thumbnails.Data″
2. Create a new UserControl.Resources
section as in Listing 19.11:
<UserControl.Resources>
<data:MediaExCollection x:Key=″TEMPMediaDataSource″
d:IsDataSource=″True″>
<data:MediaEx MediaName=″pic1.png″
Type=″Image″ Description=″Test1″ />
<data:MediaEx MediaName=″pic2.jpg″
Type=″Image″ Description=″Test2″ />
</data:MediaExCollection>
</UserControl.Resources>
3. Replace the existing ItemsControl
in the XAML markup with Listing 19.12, including a data binding to our test data source:
<ItemsControl x:Name=″ThumbItemsControl″
ItemsSource=″{Binding Source={StaticResource TEMPMediaDataSource}}″ />
If you click on the Preview tab in Visual Studio (or if you run the application), you should now see two lines in our ThumbnailsViewerControl
, each with the text “Thumbnails.Data.MediaEx” (see Figure 19.2). Since we worked with data controls, we know that this occurs when the ItemsControl
has items, but no template to represent them. So we can be happy, our “wiring” to the test data is working fine.
Figure 19.2 ThumbnailsViewerControl
in Preview tab
By default, the ItemsControl
uses a vertical StackPanel
to display the items. This is exactly what we need, so no need to touch anything. Should you want to use another panel (for example a horizontal StackPanel
) someday, you can do so in Blend with the following steps. Note that this is not needed here, it’s just so that you see this functionality once:
1. In Blend, in Page.xaml, right-click on the ThumbnailsViewer control and choose Edit Control. This opens ThumbnailsViewerControl.xaml.
2. Right-click on the ItemsControl
and select Edit Other Templates, Edit Items Panel, Create Empty.
3. Name the new template ThumbnailsPanelTemplate and click OK.
4. In the template, right-click on the Grid
and select Change Layout Type, StackPanel
.
5. Select the StackPanel
, and set its Orientation
to Horizontal in the Properties panel. Then set the scope back to the UserControl
.
Now we “just” need to create a visual representation of the items.
DataTemplate
In fact, we want to use a similar representation to what we had until now. To make it a little simpler, we will, however, remove the reflection next to the Thumbnails. Also, one single template must be able to handle a video as well as an image!
The first action is to create a DataTemplate
and to implement it so that it handles images, with the following steps:
1. In Blend, in ThumbnailsViewerControl.xaml, right-click on ThumbItemsControl and choose Edit Other Templates, Edit ItemTemplate, Create Empty.
2. Name this new DataTemplate
ThumbnailTemplate and place it in the UserControl
.
3. Select the main Grid
in the template. Then, using the Search box on top of the Properties panel, find the Style
property and set it to ThumbnailGridStyle. Use the small square next to the Style
property, then select Local Resource from the context menu. This Style
is the one we created in Chapter 17. Don’t worry if the Grid
appears smaller than it should, this is a Blend issue and we will take care of that in a minute.
4. Inside the Grid
, add a Border
and reset its Width
, Height
, HorizontalAlignment
, VerticalAlignment
and Margin
to their default value. Remember to use the small square next to the property and choose Reset.
5. Set the Border
’s Style
to ThumbnailBorderStyle using the same process as in step 3. This Style
was also created in Chapter 17 and placed in App.xaml.
6. Inside the Border
, add an Image
control (you must select it from the Asset library with the Show All check box checked). If needed, reset the Image
’s Width
and Height
, Margin
and alignment.
7. Define an empty Style
for the Image
, name it ThumbnailImageStyle and place it in the Application. In the Style
, set the Image
’s Stretch
property to Fill.
8. Save and set the scope back to the DataTemplate
. Then, using the small square next to the Source
property of the Image
, set a DataBinding
to the Explicit Data Context, with the custom path expression set to ImageSource
.
9. Build the application in Blend. You should now see two images in the ItemsControl
. See how handy it is to design the DataTemplate
when you can actually see it? If you want, edit the Styles
to make the thumbnails look just like you want them.
10. With the Image
selected, add an event handler (use the small “lightning bolt” button located on top of the Properties panel). Enter the name media_MouseLeftButtonDown
for the MouseLeftButtonDown
event. Then click out of the text box to create the event in Visual Studio. We implement this event handler later.
Our DataTemplate
can handle pictures now, but we also have videos in the collection. Implement the following steps:
1. Go back to Blend and in the ThumbnailTemplate, add another Border
in the main Grid
. Make it fill the whole space like before.
2. Using the Properties panel, set the new Border
’s Style
to the local resource we created earlier, named ThumbnailBorderStyle (the small square next to the Style
property helps you there).
3. In the Border
, add a MediaElement
from the Asset library. Set its Width
and Height
to Auto, set Stretch
to Fill, and set AutoPlay
to False. Remember that due to a bug in Blend, you might have to click on the check box a couple of times until the value gets set correctly. See in the XAML file whether the value AutoPlay
=″False″
is written.
4. Using the small square next to the Source
property, set a data binding to Explicit Data Context, with the “custom path expression” set to MovieUri
.
5. Here, too, set the MouseLeftButtonDown
event to the same event as before: media_MouseLeftButtonDown
.
6. In addition, set the MediaEnded
event to an event handler named MediaElement_MediaEnded
. Remember we implemented this event handler before (in Chapter 5, “Using Media”).
7. Open Page.xaml.cs in Studio and locate the old event handler MediaElement_MediaEnded
. Then copy the content into the new event handler with the same name in ThumbnailsViewerControl.xaml.cs. You can delete the old event handler in Page.xaml.cs.
Again, you should build the application as a check that everything is fine.
We can now remove the test markup we added before with the following steps:
1. In Visual Studio, remove the TEMPMediaDataSource
collection from ThumbnailsViewerControl.xaml.
2. Remove the ItemsSource
property from the ItemsControl
. We don’t need this anymore. The application should build just fine. You can run it, but nothing will appear yet.
For the moment, our media files are embedded inside the assembly Thumbnails.dll. This is obviously far from ideal. The first task is to remove the files and place them on the web server instead, with the following steps:
1. In Visual Studio, find the folder ClientBin and create a new folder inside it. Name this new folder MediaFiles.
2. Drag the files mov1.wmv, pic1.png, pic2.jpg, and pic3.jpg from Thumbnails to Thumbnails.Web/ClientBin/MediaFiles. This creates a copy of the files, so you can now delete them from the Thumbnails project.
3. Before you build the application, right-click on the Thumbnails project in the Solution Explorer and select Open Folder in Windows Explorer. Navigate to ThumbnailsinDebug and write down the size of Thumbnails.dll. Then, build the application in Studio and check the size again. Because all the media files are now removed from the DLL, the size should be much smaller.
4. Since we don’t have the local media files anymore, open Page.xaml and remove the TextBlock.Foreground
for both title TextBlocks
(the ones saying “Welcome to my gallery” and “Have some fun”). We will set these in code instead.
5. While we are at it, and because we want to access these TextBlocks
in code, let’s name them TitleTextBlock1
and TitleTextBlock2
.
6. Then, add an ImageBrush
in code. Add the code in Listing 19.13 in Page.xaml.cs, at the end of the Page
constructor. Then save all the files and build the application.
MediaExCollection mediaCollection
= Resources[″MediaDataSource″] as MediaExCollection;
MediaEx mediaInfo1 = mediaCollection[1];
MediaEx mediaInfo2 = mediaCollection[2];
ImageBrush brush = new ImageBrush();
brush.ImageSource = mediaInfo1.ImageSource;
TitleTextBlock1.Foreground = brush;
brush = new ImageBrush();
brush.ImageSource = mediaInfo2.ImageSource;
TitleTextBlock2.Foreground = brush;
We create two different ImageBrushes
according to the images we have in the MediaEx
instances that we read from the resources. This is temporary and will change in Chapter 22.
Remember the ItemsSource
DP that we added into our ThumbnailsViewerControl before? We will use this DP to connect the control to the real data files. Since we internally “wire” the ItemsControl
to this DP, this should work just fine. Follow the steps:
1. Open Page.xaml in Blend and click on ThumbnailsViewer.
2. In the Properties panel, look for the ItemsSource
.
3. Use the small square next to this property to set the value to a data binding. Select the MediaDataSource
in the Data Sources panel. Then click Finish. We see the MediaDataSource
in the Create Data Binding dialog because we set the d:IsDataSource
attribute to True
earlier.
At this point, you should see four empty borders in the ItemsControl
in Blend. They are empty because the actual media files are not in the Silverlight project anymore (we moved them in the section titled “Moving the Media Files” earlier). If you run the application (and make sure that you set the website project Thumbnails.Web and index.html as Startup), you will see the four media thumbnails.
We don’t do anything yet when a media gets clicked, but we have an event handler already. What we want is simple: We just need to raise the SelectionChanged
event.
UserControl
In Visual Studio, in ThumbnailsViewerControl.xaml.cs, get the clicked element and the corresponding MediaEx
instance. Thankfully we know that the data item is automatically set as the DataContext
of the FrameworkElement
, and that it is passed to all its children. We already have an empty method for the media_MouseLeftButtonDown
event. You can now replace it with Listing 19.14.
private void media_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
FrameworkElement clickedElement = sender as FrameworkElement;
MediaEx media = clickedElement.DataContext as MediaEx;
OnSelectionChanged(clickedElement, media);
}
When an item gets clicked, we want the animation to start, the Thumbnail to expand and be displayed in the frame, the movie to start running, and so on. In short, we want the method named media_MouseLeftButtonDown
in Page.xaml.cs to be executed. We can do this with the following steps:
1. This method was initially an event handler, but it’s not anymore. Let’s modify its name to make this more obvious. We can also change its signature:
void ExpandCollapseMedia(FrameworkElement castedSender)
2. Because we now pass the castedSender
directly to the method, we don’t need the first line of the method, the line where the sender
was casted. Remove that line.
3. The name castedSender
doesn’t make much sense anymore, so here is a trick to change it everywhere in the code in two easy steps:
In the method signature, rename castedSender
to media
.
Press Shift+Alt+F10 and select Rename ‘castedSender’ to ‘media’. This replaces the name everywhere in the method. You can also use this trick for attributes, properties, methods, and have Studio replace the name everywhere in the application. If you get a message from Studio, just click on “Continue”.
4. Try to build now: You should get a compilation error. Remember that we did the “wiring” to the MouseLeftButtonDown
event handler in code before. You need to remove this part. In the Page
constructor, delete the lines from
UIElement media = null;
to
while (media != null);
Finally, let’s hook the SelectionChanged
event of the ThumbnailsViewer control to the ExpandCollapseMedia
method.
1. At the end of the Page
constructor, add the event handler as in Listing 19.15:
ThumbnailsViewer.SelectionChanged
+= new SelectionChangedEventHandler(ThumbnailsViewer_SelectionChanged);
2. And then implement it as in Listing 19.16:
void ThumbnailsViewer_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
ExpandCollapseMedia(e.AddedItems[0] as FrameworkElement);
}
}
You can now build and run the application, and click on the Thumbnails. We restored the functionality we had before, but this time in a much more flexible and extendable way. Note that the ScrollViewer
doesn’t scroll yet. We will take care of that in Chapter 20.
Custom controls have a different purpose than user controls.
User controls are a group of controls fulfilling a certain purpose. It is more of a logical encapsulation of functionality within an application. A user control is not lookless; it has a XAML front-end.
Custom controls are reusable UI elements designed as lookless objects. They do not have a XAML front end (but a template can be attached to them to define their look and feel). They are usually distributed in a separate assembly and referenced from the application.
In practice, you can also store user controls in a separate assembly and reuse them in multiple applications. This separation in custom controls versus user controls is also a matter of personal preference, technical expertise, what you really want to do with the control, and so on. Generally, user controls are easier and faster to develop than custom controls, but they do not offer the same level of separation between behavior and appearance as custom controls.
Creating a new custom control from scratch is a big job and requires a good understanding of Silverlight. This section gives you an overview of how to build a simple custom control, but you might want to explore more on your own. In that case, check out the “Digging Deeper” section in Chapter 20.
The control we create now displays information about media. In this simple implementation, we only provide a short description and a long description. Of course the control can be extended later to display a name, date, and other information for the current media. For the moment, we will not worry about the way the information will be displayed. This will be for later, when we wear our “designer’s hat” and create a template for the control. For now, we only worry about the implementation. The control must do the following:
1. Accept an instance of the class Media
that we created earlier.
2. Provide the Media
.Description
as a LongDescription
property.
3. Create a ShortDescription
property, calculated according to a maximum number of characters.
4. Should these properties change, the controls bound to them must update their view automatically (you see where I am going with that, don’t you?).
5. Be encapsulated and easily reusable in other applications if needed.
6. Provide a simple default look and feel as a designer will create a new template anyway.
Now that we have a better idea of what our control must fulfill, let’s implement!
To fulfill requirement number 5, we want to place the MediaInfoDisplay
control in a class assembly. Also, due to the relationship between the control and the Media
data object, let’s put MediaInfoDisplay
in the MediaInfo assembly. Follow the steps:
1. In Visual Studio, add a new class to the project MediaInfo. Name it MediaInfoDisplay.cs.
2. Let the new control derive from the Control
class:
public class MediaInfoDisplay : Control
3. In the class, declare a constant. This is the number of characters that the ShortDescription
will display. Note that, in this simple implementation, we do not provide a way for the user to change this value. Constants are static, even though they are not declared explicitly so. A constant always has the same value for every instance of this type.
private const int SHORT_LENGTH = 30;
4. Add a default constructor to the class as in Listing 19.17. The only operation is to specify the DefaultStyleKey
property.
public MediaInfoDisplay()
{
DefaultStyleKey = typeof(MediaInfoDisplay);
}
This property is inherited from the FrameworkElement
class.
It notifies the framework that if nothing else is specified, it must look for a Style
corresponding to this control’s type, and apply it.
We use this later to create a default look and feel for the MediaInfoDisplay
control and include it in our class library.
We now specify what parts and states the control will contain. This is not strictly needed by the Silverlight framework itself, but will enable a good workflow in Blend. This also helps us visualize what we need to do. Parts are components of the control. Typically, a part is an element to which event handlers will be attached. A complex control can have many parts. A more simple control might have no parts at all. Our control will have one part—one element that can be clicked—and we will call it the DescriptionPanel
.
Parts and states are specified using attributes. Remember them? We talked about them in Chapter 14, “Letting .NET and JavaScript Talk,” and mentioned they are used to decorate classes or other elements with information. That’s what we do here. Expression Blend reads this metainformation and reacts accordingly. On top of the class declaration, add an attribute for the DescriptionPanel
part:
[TemplatePart(Name=″DescriptionPanel″, Type=typeof(FrameworkElement))]
We mention that DescriptionPanel
will be a FrameworkElement
. For example, it could be a Border
, Grid
, or anything the designer wants to use. We don’t want to restrain the designer’s creativity by restricting the type of the DescriptionPanel
part.
Now we use another attribute to specify the control’s states. We use two states groups: CommonStates
gathers states that have to do with the general appearance of the control. DescriptionStates
gathers states that have to do with the way the description is displayed.
For this last group, we specify two states: The control displays either a ShortDescription
or a LongDescription
(that’s requirement numbers 2 and 3 in the previous list). So we specify DescriptionNormal
for when the ShortDescription
is displayed, and DescriptionExpanded
for when the LongDescription
is shown.
In addition we define a Normal
state and a MouseOver
state, which are pretty much self-explanatory.
Add the following attributes right below the TemplatePart
one and before the class declaration, so that we have Listing 19.18:
[TemplatePart(Name=″DescriptionPanel″, Type=typeof(FrameworkElement))]
[TemplateVisualState(Name = ″Normal″, GroupName = ″CommonStates″)]
[TemplateVisualState(Name = ″MouseOver″, GroupName = ″CommonStates″)]
[TemplateVisualState(Name = ″DescriptionNormal″,
GroupName = ″DescriptionStates″)]
[TemplateVisualState(Name = ″DescriptionExpanded″,
GroupName = ″DescriptionStates″)]
public class MediaInfoDisplay : Control
DependencyProperties
To fulfill the functionality, our control needs to expose three DPs. We define them now.
1. Add a DP for the ShortDescription
property as in Listing 19.19:
// Short description DP
public string ShortDescription
{
get { return (string) GetValue(ShortDescriptionProperty); }
set { SetValue(ShortDescriptionProperty, value); }
}
public static readonly DependencyProperty ShortDescriptionProperty
= DependencyProperty.Register(″ShortDescription″,
typeof(string), typeof(MediaInfoDisplay),
new PropertyMetadata(null));
2. When the LongDescription
is changed, the ShortDescription
should be updated too. Let’s do this by creating an event handler for the PropertyChangedCallback
event as in Listing 19.20. We see in step 3 how to “wire” this event handler to the event.
// Long description DP
private static void OnLongChanged(DependencyObject d,
DependencyPropertyChangedEventArgs args)
{
MediaInfoDisplay sender = d as MediaInfoDisplay;
sender.ShortDescription
= (sender.LongDescription.Length > SHORT_LENGTH)
? sender.LongDescription.Substring(0, SHORT_LENGTH - 3) + ″...″
: sender.LongDescription;
}
We calculate the value of the ShortDescription
for the sender based on the LongDescription
and on the constant value SHORT_LENGTH
.
3. Now register the DP for the long description (Listing 19.21):
public string LongDescription
{
get { return (string) GetValue(LongDescriptionProperty); }
set { SetValue(LongDescriptionProperty, value); }
}
public static readonly DependencyProperty LongDescriptionProperty
= DependencyProperty.Register(″LongDescription″,
typeof(string), typeof(MediaInfoDisplay),
new PropertyMetadata(new PropertyChangedCallback(OnLongChanged)));
Notice how the first parameter of the PropertyMetadata
is a PropertyChangedCallback
, and how it is “wired” to our event handler. This way, every time that LongDescription
changes, the ShortDescription
will also be updated.
The last DP we need is an instance of the class Media
that our control will represent. We must handle changes to this property. We set the LongDescription
DP according to the instance, and in turn this automatically updates the ShortDescription
.
Add the following event handler and register the DP as in Listing 19.22:
// Media info DP
private static void OnMediaChanged(DependencyObject d,
DependencyPropertyChangedEventArgs args)
{
MediaInfoDisplay sender = d as MediaInfoDisplay;
Binding binding = new Binding();
binding.Source = args.NewValue as Media;
binding.Path = new PropertyPath(″Description″);
sender.SetBinding(LongDescriptionProperty, binding);
}
public Media MediaInfo
{
get { return (Media) GetValue(MediaInfoProperty); }
set { SetValue(MediaInfoProperty, value); }
}
public static readonly DependencyProperty MediaInfoProperty
= DependencyProperty.Register(″MediaInfo″,
typeof(Media),
typeof(MediaInfoDisplay),
new PropertyMetadata(new PropertyChangedCallback(OnMediaChanged)));
We data bind the LongDescription
to the property Media
.Description
, which raises the PropertyChanged
event when it is set. So we will be automatically notified if it changes. For example, if later you decide to implement a user interface to let the user edit the media description, the rest of the UI will be updated according to the user’s changes! At this point, build the application to check if the code has errors.
The states of the control are managed by a neat class called VisualStateManager
or VSM. This class provided by the Silverlight framework handles all the transitions between the control’s states. Of course it needs input from us:
We need to trigger the transition when the event handlers are raised.
We also need to tell the VSM what the transitions are. We will customize these transitions later when we put our designer’s hat on and create a template for our MediaInfoDisplay
control.
To make things easier, we create a couple of private attributes and a helper method in the class MediaInfoDisplay
. Add two private attributes holding the control’s desired state and create a helper method named GoToState
as in Listing 19.23:
// Handling the states
private bool _isMouseOver = false;
private bool _isDescriptionExpanded = false;
private void GoToState(bool useTransitions)
{
if (_isMouseOver)
{
VisualStateManager.GoToState(this,
″MouseOver″, useTransitions);
}
else
{
VisualStateManager.GoToState(this,
″Normal″, useTransitions);
}
if (_isDescriptionExpanded)
{
VisualStateManager.GoToState(this,
″DescriptionExpanded″, useTransitions);
}
else
{
VisualStateManager.GoToState(this,
″DescriptionNormal″, useTransitions);
}
}
Depending on the desired states, the VSM changes the control to the corresponding state.
Note the parameter useTransitions
: If it is true
, the Silverlight framework will set the state of the control using an animation. If it’s false
, the transition will be immediate. This can be useful to place the control in its initial state, for example. We see a little later how to set the transitions.
Be careful here: the names of the states you use here must match the names of the states you declared for the class in the attributes before. This is annoying, but it’s the way it is. Build the application to check is everything if OK.
Our control declared one part, which is the element (of type FrameworkElement
) containing the description strings. It can be a Border
or a panel of any kind, depending on the graphics designer’s fantasy. We declare it as a part, because we want to change the state of the description when this panel is clicked. The control’s state can go from DescriptionNormal
to DescriptionExpanded
.
Let’s handle the case where the part is found. In that case, we must add event handlers to it (Listing 19.24):
// Handling the part
1 private FrameworkElement _panelPart = null;
2 private FrameworkElement DescriptionPanelPart
3 {
4 get { return _panelPart; }
5 set
6 {
7 FrameworkElement _oldPart = _panelPart;
8 if (_oldPart != null)
9 {
10 _oldPart.MouseEnter
11 -= new MouseEventHandler(_panelPart_MouseEnter);
12 _oldPart.MouseLeave
13 -= new MouseEventHandler(_panelPart_MouseLeave);
14 _oldPart.MouseLeftButtonDown
15 -= new MouseButtonEventHandler(
16 _panelPart_MouseLeftButtonDown);
17 }
18
19 _panelPart = value;
20 if (_panelPart != null)
21 {
22 _panelPart.MouseEnter
23 += new MouseEventHandler(_panelPart_MouseEnter);
24 _panelPart.MouseLeave
25 += new MouseEventHandler(_panelPart_MouseLeave);
26 _panelPart.MouseLeftButtonDown
27 += new MouseButtonEventHandler(
28 _panelPart_MouseLeftButtonDown);
29 }
30 }
31 }
Line 7 gets the part that we saved before (in case it’s not the first call). If it’s not null
, we remove the event handlers that we had attached to it (lines 10 to 16). That’s a bit confusing, but check lines 22 to 28; this is where we attach the event handlers.
On line 19, we assign the value
to the private attribute, saving it for later.
In case the panelPart
is not null
, we assign new event handlers to it. We handle the case where the mouse passes over the panel, where it exits the panel, and finally where the element is clicked.
Your control should always handle the case where the part is null
. After all, nothing forces the designer to use the named part in a template. Your control must be robust enough and avoid crashing if that’s the case.
Finally, let’s implement the event handlers (see Listing 19.25). Fortunately, it’s easy thanks to our helper method GoToState
.
// Event handlers
void _panelPart_MouseEnter(object sender,
MouseEventArgs e)
{
_isMouseOver = true;
GoToState(true);
}
void _panelPart_MouseLeave(object sender,
MouseEventArgs e)
{
_isMouseOver = false;
GoToState(true);
}
void _panelPart_MouseLeftButtonDown(object sender,
MouseButtonEventArgs e)
{
_isDescriptionExpanded = !_isDescriptionExpanded;
GoToState(true);
}
We ask the method GoToState
to use transitions when the events are raised: The user interaction should cause smooth transitions!
The “expanding” of the description is a toggle between two states. So every time the panel is clicked, we toggle the boolean
value _isDescriptionExpanded
.
At this point, build the application again.
Finally (and yes, that’s the last operation in this class), we need to trigger all these actions! The right moment to do so is when the ControlTemplate
is applied to the control. When this occurs, the Silverlight framework calls the method OnApplyTemplate
on the Control
class that we derive from.
However, if we don’t do anything, the Control
parent class doesn’t notify us when this happens. We need to override this method and redefine its behavior as in Listing 19.26.
// Applying the template
1 public override void OnApplyTemplate()
2 {
3 base.OnApplyTemplate();
4 DescriptionPanelPart
5 = (FrameworkElement) GetTemplateChild(″DescriptionPanel″);
6 GoToState(false);
7 }
On line 3, we call the base class’s method. This is needed, because in addition to performing our own operations, the base class needs to do the default tasks such as applying the template and wiring up everything.
On lines 4 and 5, we get the named part, using the Control
’s method GetTemplateChild
. Here, too, the name of the part must match the name you declared in the beginning with the TemplatePart
attribute. We assign this value to our DescriptionPanelPart
property, which wires up the event handlers to the part (except if it is null!).
GetTemplateChild
is a protected method of the Control
class. It means that you can use it only in a derived class (this is the case here). You cannot use this method outside the MediaInfoDisplay
class.
Then on line 6 we instruct the VSM to set the control in its default state. This stage is needed, because we want the control to be in a defined state, so that all the transitions run correctly when the user passes the mouse over it or clicks it.
If anyone wants to use our control now, well they can’t yet: It is totally lookless. Creating a look and feel for this control will be done in Chapter 20. First we create a generic style and template for the control and embed it in the same assembly. Then we use our control in the Thumbnails application and wear our designer’s hat to create a slightly more complex template for the control. You can, however, build and run the application.
You were warned: creating controls is not easy. In this chapter we performed a lot of ground work, but our controls are neither finished nor functional yet. We continue working on them in the next chapter!
3.145.64.126