Chapter 19 Creating User Controls and Custom Controls

In This Chapter

Creating a Thumbnails Viewer User Control

Creating the Items and Designing a DataTemplate

Creating a MediaInfoDisplay Custom Control

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.

Creating a Thumbnails Viewer User Control

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.

Creating the Data Objects

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.

Making the Parent Class 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:

Listing 19.1 MediaType Enumeration and Media Class

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

Image First we add an enumeration named MediaType that we will use to define the type of the current media.

Image 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.

Image 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.

Extending the Media Class

The 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:

Listing 19.2 MediaEx Class

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

Image None of these properties change during runtime, so they don’t raise the PropertyChanged event.

Image 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 ′/′.

Image 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.

Listing 19.3 MediaExCollection Class

public class MediaExCollection : ObservableCollection<MediaEx>
{
}

Packing the 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

Image

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.

Communicating with the Outside World

Our UserControl should get some information from the Page, and also expose information when an item is selected.

Adding an ItemsSource Property

We 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.

Listing 19.4 OnItemsSourceChanged Event Handler

private static void OnItemsSourceChanged(DependencyObject d,
     DependencyPropertyChangedEventArgs args)
{
     ThumbnailsViewerControl sender = d as ThumbnailsViewerControl;
     sender.ThumbItemsControl.ItemsSource
        =  (args.NewValue as MediaExCollection);
}

Image 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.

Image 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:

Listing 19.5 ItemsSourceProperty DP

1  public static readonly DependencyProperty ItemsSourceProperty
2       = DependencyProperty.Register(″ItemsSource″,
3       typeof(MediaExCollection), typeof(ThumbnailsViewerControl
4       new PropertyMetadata(new PropertyChangedCallback(OnItemsSourceChanged)));

Image On line 1, we declare the name of the DP.

Image Line 2 is the call to the static method Register on the DependencyProperty class. The first parameter is the name of the property.

Image Line 3 states the type of the DP, and then the type of the owner.

Image 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.

Listing 19.6 Convenience Setter and Getter

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.

Adding a SelectedItem DependencyProperty

Add the code as shown in Listing 19.7:

Listing 19.7 SelectedItem DP

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.

Adding an Event

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

Listing 19.8 OnSelectionChanged Helper Method

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

Image For more information, check the class SelectionChangedEventArgs in the SDK documentation.

Image 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!

Creating the Items and Designing a 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.

Creating the Media Instances in XAML

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.

Listing 19.9 Adding Namespaces

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:

Listing 19.10 MediaDataSource Collection

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

Image 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.

Image The four instances are added to an instance of the MediaExCollection class we created before. Remember that this class inherits ObservableCollection.

Image 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:

Listing 19.11 TEMPMediaDataSource Collection

<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:

Listing 19.12 Data Bound ThumbItemsControl

<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

Image

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.

Designing a 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!

Handling Images

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.

Handling Videos

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.

Removing the Test Data Source

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.

Moving the Media Files

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.

Listing 19.13 Setting the TextBlocks’ Brushes in Code

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.

Connecting the ThumbnailsViewer to the Real Data

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.

Raising and Handling the SelectionChanged Event

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.

Raising the Event in the 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.

Listing 19.14 media_MouseLeftButtonDown Event Handler

private void media_MouseLeftButtonDown(object sender,
     MouseButtonEventArgs e)
{
     FrameworkElement clickedElement = sender as FrameworkElement;
     MediaEx media = clickedElement.DataContext as MediaEx;
     OnSelectionChanged(clickedElement, media);
}

Handling the Event in the Page

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:

Image In the method signature, rename castedSender to media.

Image 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:

Listing 19.15 Adding the SelectionChanged Event Handler

ThumbnailsViewer.SelectionChanged
    +=  new SelectionChangedEventHandler(ThumbnailsViewer_SelectionChanged);

2. And then implement it as in Listing 19.16:

Listing 19.16 ThumbnailsViewer_SelectionChanged Event Handler

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.

Creating a MediaInfoDisplay Custom Control

Custom controls have a different purpose than user controls.

Image 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.

Image 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.

Making a Blueprint

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!

Creating the “Shell”

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.

Listing 19.17 MediaInfoDisplay Constructor

public MediaInfoDisplay()
{
     DefaultStyleKey = typeof(MediaInfoDisplay);
}

Image This property is inherited from the FrameworkElement class.

Image 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.

Image We use this later to create a default look and feel for the MediaInfoDisplay control and include it in our class library.

Specifying the Parts and States

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:

Listing 19.18 Parts and States

[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

Adding 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:

Listing 19.19 ShortDescription DP

// 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.

Listing 19.20 OnLongChanged Event Handler

// 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):

Listing 19.21 LongDescription DP

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:

Listing 19.22 MediaInfo DP

// 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.

Handling the States

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:

Image We need to trigger the transition when the event handlers are raised.

Image 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:

Listing 19.23 GoToState Method

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

Image Depending on the desired states, the VSM changes the control to the corresponding state.

Image 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.

Handling the Part

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

Listing 19.24 DescriptionPanelPart Property

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

Image 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.

Image On line 19, we assign the value to the private attribute, saving it for later.

Image 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.

Image 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.

Listing 19.25 Event Handlers

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

Image We ask the method GoToState to use transitions when the events are raised: The user interaction should cause smooth transitions!

Image 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.

Applying the Template

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.

Listing 19.26 Applying the Template

        // Applying the template
1     public override void OnApplyTemplate()
2     {
3          base.OnApplyTemplate();
4          DescriptionPanelPart
5              = (FrameworkElement) GetTemplateChild(″DescriptionPanel″);
6          GoToState(false);
7     }

Image 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.

Image 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!).

Image 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.

Image 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.

Summary

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!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.64.126