Part 4: Image Detail View
The next step in the search app was to show the the detail image. Here is what it looks like.
Building this was pretty straight forward. Few things to call out…
- One of the interesting things was the fact that I had switched my grid view to use the thumbnail images but I really wanted to show the full size images in the detail view. Full size images are not downloaded yet. Since Silverlight uses Browser to download, when I show the grid view all the images shown in grid view are already downloaded and cached in browser cache but the full size images are not downloaded. I played around with some animations to show while full image is downloaded but since downloadcompleted event does not exist on image, it was kind of tricky to know when to start animation. Finally the approach I went with was to show the thumbnail image (that is already downloaded) to begin with and then switch the fullsize image when it is already downloaded. It also gives “Deep Zoom” like effect where sometime you can see image crystallize when fullsize image replaces the thumbnail version.
- The only reason why I exposed the SearchResult property on this control was because I wanted to use databinding but also needed to know when the DataContext changes. Since in SL2 there are not DataContext change notifications, I exposed the property and set the datacontext myself.
- One of the data points that I wanted to display in detail view was link to actual image. I used HyperlinkButton control so that user can click on it and new browser window opens up to show the image. I used TargetName property exposed on this control. I also ended up building another IValueConverter so that I can convert string into Uri.
- I needed to make changes to ImageGridView since I needed to know which items in the grid was selected.
UriConverter
1: using System;
2: using System.Windows.Data;
3:
4: namespace Search
5: {
6: /// <summary>
7: /// Converts string into Uri
8: /// </summary>
9: public class UriConverter : IValueConverter
10: {
11: #region Public Members
12:
13: public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
14: {
15: return (new Uri((string)value, UriKind.RelativeOrAbsolute));
16: }
17:
18: public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
19: {
20: return ((Uri)value).ToString();
21: }
22: #endregion
23: }
24: }
This class converts string returned by search service into Uri so that I can databind hyperlinkbutton’s NavigateUri property to it.
DetailImage
Here is the xaml that describes the detailimage control. It shows Size in bytes, height/width in pixels, link to image that opens in new browser window and actual image.
1: <UserControl x:Class="Search.DetailImage"
2: xmlns="http://schemas.microsoft.com/client/2007"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5: xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6: xmlns:control="clr-namespace:Search;assembly=Search"
7: mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="300"
8: >
9: <UserControl.Resources>
10: <control:UriConverter x:Key="uriconverter"/>
11: <control:ImageSourceConverter x:Key="imagesourceconverter"/>
12: </UserControl.Resources>
13:
14: <StackPanel x:Name="LayoutRoot">
15: <StackPanel Orientation="Horizontal">
16: <TextBlock Text="URL: " FontWeight="Bold" Margin="10,0,0,0"/>
17: <HyperlinkButton Content="Link to Image"
18: FontSize="15"
19: NavigateUri="{Binding Image.ImageURL,Converter={StaticResource uriconverter}}"
20: TargetName="_blank"/>
21: </StackPanel>
22: <StackPanel Orientation="Horizontal">
23: <TextBlock Text="Width (px): " FontWeight="Bold" Margin="10,0,0,0"/>
24: <TextBlock x:Name="txtWidth" Text="{Binding Image.ImageWidth}"/>
25: </StackPanel>
26: <StackPanel Orientation="Horizontal">
27: <TextBlock Text="Height (px):" FontWeight="Bold" Margin="10,0,0,0"/>
28: <TextBlock x:Name="txtHeight" Text="{Binding Image.ImageHeight}"/>
29: </StackPanel>
30: <StackPanel Orientation="Horizontal">
31: <TextBlock Text="Size: (Bytes):" FontWeight="Bold" Margin="10,0,0,0"/>
32: <TextBlock x:Name="txtSize" Text="{Binding Image.ImageFileSize}"/>
33: </StackPanel>
34: <Border
35: BorderThickness="2,2,2,2" CornerRadius="5,5,5,5"
36: BorderBrush="#FF000000"
37: Margin="0,20,0,0" Visibility="Collapsed"
38: x:Name="fullborder">
39:
40:
41: </Border>
42: <Border
43: BorderThickness="2,2,2,2" CornerRadius="5,5,5,5"
44: BorderBrush="#FF000000"
45: Margin="0,20,0,0" x:Name="imageborder">
46:
47: <Image
48: Margin="20,20,20,20" Stretch="Fill"
49: Source="{Binding Image.ThumbnailURL , Converter={StaticResource imagesourceconverter}}"
50: RenderTransformOrigin="0.5,0.5"
51: x:Name="image"
52: HorizontalAlignment="Stretch">
53: <Image.RenderTransform>
54: <TransformGroup>
55: <ScaleTransform/>
56: <SkewTransform/>
57: <RotateTransform/>
58: <TranslateTransform/>
59: </TransformGroup>
60: </Image.RenderTransform>
61: </Image>
62: </Border>
63: </StackPanel>
64: </UserControl>
Here is the code behind for this user control.
1: using System;
2: using System.Windows;
3: using System.Windows.Controls;
4: using System.Windows.Media.Imaging;
5: using Search.SearchService;
6:
7: namespace Search
8: {
9: public partial class DetailImage : UserControl
10: {
11: #region Public Members
12:
13: public DetailImage()
14: {
15: InitializeComponent();
16: _fullimage = new System.Windows.Controls.Image();
17: _fullimage.Margin = new Thickness(20);
18: fullborder.Child = _fullimage;
19: }
20:
21: //sets the SearchResult. The reason why I had to expose this is because
22: //I need to know when the data context is changing so that I can do necessary
23: //work to switch images from thumbnail to fullsize
24: public Result SearchResult
25: {
26: get
27: {
28: return _searchresult;
29: }
30: set
31: {
32: _searchresult = value;
33: //set the datacontext for the control
34: DataContext = value;
35: imageborder.Visibility = Visibility.Visible;
36: fullborder.Visibility = Visibility.Collapsed;
37: BitmapImage bi = new BitmapImage(new Uri(_searchresult.Image.ImageURL, UriKind.Absolute));
38: bi.DownloadProgress += new DownloadProgressEventHandler(bi_DownloadProgress);
39: bi.ImageFailed += new ExceptionRoutedEventHandler(bi_ImageFailed);
40: _fullimage.Source = bi;
41: }
42: }
43:
44:
45: #endregion
46:
47: #region Private Members
48:
49: //when the full image is downloaded
50: //I switch the thumbnail image in detail view for full size
51: void bi_DownloadProgress(object sender, DownloadProgressEventArgs e)
52: {
53: if (e.Progress == 1)
54: {
55: imageborder.Visibility = Visibility.Collapsed;
56: fullborder.Visibility = Visibility.Visible;
57: }
58: }
59:
60: //If for some reason full size image does not download
61: //then stick with thumbnail
62: void bi_ImageFailed(object sender, ExceptionRoutedEventArgs e)
63: {
64: imageborder.Visibility = Visibility.Visible;
65: fullborder.Visibility = Visibility.Collapsed;
66: }
67:
68: Search.SearchService.Result _searchresult;
69: System.Windows.Controls.Image _fullimage;
70:
71: #endregion
72:
73: }
74: }
There are two borders in the XAML. One is to show the thumbnail image and other one is to show the full image. The only reason why I ended up doing that was because, in Beta1, I could not change the border’s child. When the fullsize image is downloaded completely. I make border that contains thumbnail collapased and make the border that contains fullsize image visible. I still see some flickers and should think about better way to do this.
ImageGridView
To make the Detail view work, I needed to know which image was selected in the grid view.
- I have exposed the SelectedImage property. I hook up LeftMouseDown/Up and MouseLeave events for each of the borders around thumbnails to know which element was clicked on
- I also decided to implement INotifiyPropertyChanged for this property so that consumer can know when the selection changed. I could have done this using some custom event (SelectedItemChanged) but this way some other control could directly databind to this change notification.
Here is the new XAML for ImageGridView
1: <UserControl x:Class="Search.ImageGridView"
2: xmlns="http://schemas.microsoft.com/client/2007"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:control="clr-namespace:Search;assembly=Search" >
5: <UserControl.Resources>
6: <control:ImageSourceConverter x:Key="imagesourceconverter"/>
7: <control:Tile x:Key="TileInfo"/>
8: </UserControl.Resources>
9: <Grid x:Name="LayoutRoot">
10: <ItemsControl Name ="outic" ItemsSource="{Binding}" VerticalAlignment="Top" >
11: <ItemsControl.ItemTemplate>
12: <DataTemplate>
13: <ItemsControl ItemsSource="{Binding}">
14: <ItemsControl.ItemsPanel>
15: <ItemsPanelTemplate>
16: <StackPanel Orientation="Horizontal"/>
17: </ItemsPanelTemplate>
18: </ItemsControl.ItemsPanel>
19: <ItemsControl.ItemTemplate>
20: <DataTemplate>
21: <Border
22: BorderThickness="2,2,2,2" BorderBrush="#FF313131"
23: CornerRadius="5,5,5,5"
24: Height="{Binding TileHeight,Source={StaticResource TileInfo}}"
25: Width="{Binding TileWidth,Source={StaticResource TileInfo}}"
26: Margin="{Binding TilePadding,Source={StaticResource TileInfo}}"
27: MouseEnter="Border_MouseEnter"
28: MouseLeave="Border_MouseLeave"
29: RenderTransformOrigin="1,1"
30: MouseLeftButtonDown="Border_MouseLeftButtonDown"
31: MouseLeftButtonUp="Border_MouseLeftButtonUp">
32: <Border.RenderTransform>
33: <TransformGroup>
34: <ScaleTransform ScaleX="1" ScaleY="1"/>
35: <SkewTransform AngleX="0" AngleY="0"/>
36: <RotateTransform Angle="0"/>
37: <TranslateTransform X="0" Y="0"/>
38: </TransformGroup>
39: </Border.RenderTransform>
40: <Border.Background>
41: <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
42: <GradientStop Color="#FFFFFFFF"/>
43: <GradientStop Color="#FF71A7BB" Offset="1"/>
44: </LinearGradientBrush>
45: </Border.Background>
46: <Image
47: Source="{Binding Image.ThumbnailURL , Converter={StaticResource imagesourceconverter}}"
48: ImageFailed="Image_ImageFailed"
49: Cursor="Hand" Opacity="0.7"
50: Margin="1,1,1,1"/>
51: </Border>
52: </DataTemplate>
53: </ItemsControl.ItemTemplate>
54: </ItemsControl>
55: </DataTemplate>
56: </ItemsControl.ItemTemplate>
57: </ItemsControl>
58: </Grid>
59: </UserControl>
Here is the new code behind for ImageGridView
1: using System;
2: using System.Windows;
3: using System.Windows.Controls;
4: using System.Windows.Media.Imaging;
5: using System.Windows.Media;
6: using System.ComponentModel;
7:
8: namespace Search
9: {
10: public partial class ImageGridView : UserControl, INotifyPropertyChanged
11: {
12: #region Public Members
13:
14: //implementation of INotifyPropertyChanged
15: public event PropertyChangedEventHandler PropertyChanged;
16:
17: //public constructor
18: public ImageGridView()
19: {
20: // Required to initialize variables
21: InitializeComponent();
22: _tileinfo = this.Resources["TileInfo"] as Tile;
23: }
24:
25: //height of each tile
26: public double TileHeight
27: {
28: get { return _tileinfo.TileHeight; }
29: set { _tileinfo.TileHeight = value; }
30: }
31:
32: //width of each tile
33: public double TileWidth
34: {
35: get { return _tileinfo.TileWidth; }
36: set { _tileinfo.TileWidth = value; }
37: }
38:
39: //padding around each tile applied as uniform thickness
40: public Double TilePadding
41: {
42: get { return _tileinfo.TilePadding.Top; }
43: set
44: {
45: //converts double value to uniform thickness value
46: _tileinfo.TilePadding = new Thickness((double)value);
47: }
48: }
49:
50: //selected image
51: public Image SelectedImage
52: {
53: get { return _selectedborder.Child as Image; }
54:
55: }
56: #endregion
57:
58: #region Private Members
59: //handle to tile settings
60: private Tile _tileinfo;
61:
62: //tracks if the mouse button is down before handling mouse button up
63: private bool _leftmousebuttondown = false;
64:
65: //tracks the previously selected item
66: private Border _selectedborder = null;
67:
68: //if iamge fails to load then placeholder image is shown
69: private void Image_ImageFailed(object sender, ExceptionRoutedEventArgs e)
70: {
71: ((Image)sender).Source = new BitmapImage(new Uri("notfound.jpg", UriKind.Relative));
72: }
73:
74: //on mouse enter, increase the size of image and set opacity to 1
75: private void Border_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
76: {
77: //hover effect
78: Border b = (Border)sender;
79: b.Child.Opacity = 1;
80: TransformGroup tg = b.RenderTransform as TransformGroup;
81: ScaleTransform st = tg.Children[0] as ScaleTransform;
82: st.ScaleX = 1.1;
83: st.ScaleY = 1.1;
84: }
85:
86: //on mouse leave, reset the size of image and set opacity to 0
87: private void Border_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
88: {
89: //reset hover effect
90: Border b = (Border)sender;
91: b.Child.Opacity = 0.7;
92: TransformGroup tg = b.RenderTransform as TransformGroup;
93: ScaleTransform st = tg.Children[0] as ScaleTransform;
94: st.ScaleX = 1;
95: st.ScaleY = 1;
96:
97: //reset the left mouse button flag if set
98: _leftmousebuttondown = false;
99:
100: }
101:
102: //tracks if the left mouse is down over the border
103: private void Border_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
104: {
105: _leftmousebuttondown = true;
106:
107: }
108:
109: //handles as click and sets the visual cue for selected items.
110: private void Border_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
111: {
112: if (_leftmousebuttondown == true)
113: {
114: //reset the border of previously selected border
115: if (_selectedborder != null)
116: {
117: _selectedborder.BorderThickness = new Thickness(2);
118: _selectedborder.BorderBrush = new SolidColorBrush(Colors.Black);
119: }
120:
121: //set the new selected border
122: _selectedborder = (Border)sender;
123: _selectedborder.BorderThickness = new Thickness(4);
124: _selectedborder.BorderBrush = new SolidColorBrush(Colors.Orange);
125: NotifyPropertyChanged("SelectedImage");
126: }
127: }
128:
129: //Implementation of INotifyPropertyChanged, it is only used for selected item property
130: private void NotifyPropertyChanged(String info)
131: {
132: if (PropertyChanged != null)
133: {
134: PropertyChanged(this, new PropertyChangedEventArgs(info));
135: }
136: }
137:
138: #endregion
139:
140: }
141: }
Page
To consume this control in the application, I needed to add it in Page.xaml. I added following line of XAML to the root grid in Page.Xaml.
1: <control:DetailImage Name="detailImage"
2: Margin="10,50,10,10" VerticalAlignment="Top"
3: Grid.Column="2" Grid.Row="2"
4: d:LayoutOverrides="HorizontalAlignment, Height"
5: Visibility="Collapsed"/>
Here are the changes to code
1: public Page()
2: {
3: InitializeComponent();
4: _searchresultcollectioncollection = new ObservableCollection<SearchResultCollection>();
5: _imagegridview = LayoutRoot.FindName("imageGrid") as ImageGridView;
6: _gridscroll = LayoutRoot.FindName("gridscroll") as ScrollViewer;
7: _imagegridview.DataContext = _searchresultcollectioncollection;
8: _progress = LayoutRoot.FindName("SearchProgressAnimation") as Storyboard;
9: _makesearchresultsvisible = LayoutRoot.FindName("MakeSearchResultVisible") as Storyboard;
10: _progress.Completed += new EventHandler(_progress_Completed);
11:
12: _detailimage = LayoutRoot.FindName("detailImage") as DetailImage;
13: _imagegridview.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(_imagegridview_PropertyChanged);
14:
15: }
1: //Property change for selected image to set the data context on detail image
2: private void _imagegridview_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
3: {
4: ImageGridView iamgegrid = sender as ImageGridView;
5: Search.SearchService.Result selectedresult = iamgegrid.SelectedImage.DataContext as Result;
6: _detailimage.SearchResult = selectedresult;
7: _detailimage.Visibility = Visibility.Visible;
8: }
Here is the link to working project.