SharePoint Dev Notes

Notes on SharePoint Development

Silverlight Image Viewer for SharePoint 2010

Leave a comment

This week I had a lot of time to experiment with new ideas – snow in Texas has a way of shutting down business. As part of my tasks for this week, I wanted to provide a flashy way to show off images stored in an image library. The idea came from an “employee club” site at a client. The group is made up of people from across the organization who participate in activities outside of the job – Special Olympics, Ranger’s Games and other social or community events.

The idea was to create a case-study for the client on how Silverlight can extend the capabilities of SharePoint and provide rich interaction with the data contained on the site. The web part will show a list of images in a left hand panel and allow the site visitor to click on the image to get more details about the photograph.

Since I’m still learning Silverlight, I started with a basic layout and some sample images from the web.

And here is the XAML source:

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:Tribridge.Silverlight.ImageViewer"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" x:Class="Tribridge.Silverlight.ImageViewer.MainPage"
    mc:Ignorable="d"
    d:DesignHeight="600" d:DesignWidth="800">
    <UserControl.Resources>
        <Style x:Key="HorizontalListBox" TargetType="ListBox">
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal"
                                    VerticalAlignment="Center"
                                    HorizontalAlignment="Center"/>
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <local:ImageDataCollection x:Key="Images" />
    </UserControl.Resources>
        <Grid x:Name="LayoutRoot" DataContext="{Binding Source={StaticResource Images}}">
        	<Grid.Background>
        		<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
        			<GradientStop Color="#FF323232" Offset="1"/>
        			<GradientStop Color="Gray"/>
        			<GradientStop Color="#FF8C8C8C" Offset="0.296"/>
        		</LinearGradientBrush>
        	</Grid.Background>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="160"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Border Grid.Column="1" Margin="8" BorderBrush="#59000000" BorderThickness="2">
        		<Grid DataContext="{Binding SelectedItem, ElementName=ImageList}">
        			<Grid.Background>
        				<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
        					<GradientStop Color="#7F646464" Offset="1"/>
        					<GradientStop Color="#7FB4B4B4" Offset="0.008"/>
        				</LinearGradientBrush>
        			</Grid.Background>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition Height="128"/>
                </Grid.RowDefinitions>
                    <Image x:Name="CurrentImage" Margin="10,10,10,10"
        				Grid.Row="0" Grid.Column="1" Grid.RowSpan="1"
        				Stretch="Uniform" VerticalAlignment="Stretch"
        				HorizontalAlignment="Stretch" Source="{Binding ImageURL}">
        				<Image.RenderTransform>
        					<CompositeTransform/>
        				</Image.RenderTransform>
        				<Image.Effect>
        					<DropShadowEffect/>
        				</Image.Effect>
        				<i:Interaction.Behaviors>
        					<ei:FluidMoveBehavior InitialTag="DataContext" FloatAbove="False" Duration="0:0:0.65">
        						<ei:FluidMoveBehavior.EaseY>
        							<CircleEase EasingMode="EaseOut"/>
        						</ei:FluidMoveBehavior.EaseY>
        						<ei:FluidMoveBehavior.EaseX>
        							<CircleEase EasingMode="EaseOut"/>
        						</ei:FluidMoveBehavior.EaseX>
        					</ei:FluidMoveBehavior>
        				</i:Interaction.Behaviors>
        			</Image>
                <TextBlock Text="{Binding Path=Caption}" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1" FontSize="22" FontWeight="Bold" FontFamily="Arial" />
            </Grid>
        	</Border>
        <ListBox x:Name="ImageList" Grid.Row="0"
                 Margin="8" SelectionChanged="ImageList_SelectionChanged" SelectedIndex="-1" ItemsSource="{Binding}" Background="#3FFFFFFF">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border BorderThickness="2" CornerRadius="0" BorderBrush="#FF000000">
                    <StackPanel Orientation="Vertical" Width="100" Height="100" VerticalAlignment="Top" HorizontalAlignment="Center">
                    	<StackPanel.Background>
                    		<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    			<GradientStop Color="#FFFFFFFF" Offset="0"/>
                    			<GradientStop Color="#FFFAFAFA" Offset="1"/>
                    		</LinearGradientBrush>
                    	</StackPanel.Background>
                            <Border Width="95" Height="70" BorderThickness="1" Margin="0,5,0,3" BorderBrush="#FF000000" Background="Black">
                            <Image Width="95" Height="70" Source="{Binding ImageURL}" Stretch="UniformToFill" Margin="0">
                            	<i:Interaction.Behaviors>
                            		<ei:FluidMoveSetTagBehavior Tag="DataContext"/>
                            	</i:Interaction.Behaviors>
                            </Image>
                            </Border>
                            <TextBlock Text="{Binding Caption}" TextAlignment="Center" FontWeight="Bold" Foreground="Black"/>
                    </StackPanel>
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</UserControl>

In the layout, I basically have a list box on the left hand side with a custom data template for the list items. On the right, there is another grid with an image and a textbox. I am not going to cover data binding in this example, but you will see there is a reference to my view model and binding set for each element in the view.

After creating the layout, I turned my attention to the model and view model. The model at this time is very simple, it has three properties that are used to display in the View.

    public class ImageData
    {
        public ImageData() { }

        public string ImageURL { get; set; }
        public string Caption { get; set; }
        public string Description { get; set; }
    }

I decided to inherit from an ObservableCollection for my collection that would be bound to the grid. This ended up with a few caveats that I’ll cover when I get to the data access layer of the project.

    public class ImageDataCollection : ObservableCollection<ImageData>
    {
        public static bool IsInDesignMode
        {
            get
            {
                return DesignerProperties.IsInDesignTool;
            }
        }

        public ImageDataCollection() {
			// Logic to load my sample data -- these are just random images from the web
            if (ImageDataCollection.IsInDesignMode)
            {
                this.Add(new ImageData { Caption = "A Bug's Life", ImageURL = "http://www.dan-dare.org/FreeFun/Images/CartoonsMoviesTV/BugsLifeWallpaper800.jpg" });
                this.Add(new ImageData { Caption = "Stress", ImageURL = "http://bluemoviereviews.files.wordpress.com/2009/05/life-6.jpg" });
                this.Add(new ImageData { Caption = "Foggy Morning", ImageURL = "http://fc09.deviantart.net/fs19/f/2007/250/3/a/A_slice_of_life_by_gilad.jpg" });
                this.Add(new ImageData { Caption = "Cadet Life", ImageURL = "http://www.uscga.edu/uploadedImages/Cadet_Life/photo_cadet_life_10.jpg" });
                this.Add(new ImageData { Caption = "Life on Mars", ImageURL = "http://scienceblogs.com/startswithabang/upload/2009/11/why_look_for_life_on_mars/life_on_mars.jpg" });
                this.Add(new ImageData { Caption = "Tranquility", ImageURL = "http://media-cdn.tripadvisor.com/media/photo-s/00/1c/de/ff/high-life-at-night.jpg" });
                this.Add(new ImageData { Caption = "Night Memory", ImageURL = "http://www.marcandangel.com/images/save-your-life.jpg" });
                this.Add(new ImageData { Caption = "Mountains", ImageURL = "http://giving.cornell.edu/guide/_images/high/agriculture-and-life-sciences.jpg" });
                this.Add(new ImageData { Caption = "Raindrops", ImageURL = "http://www.smashingapps.com/wp-content/uploads/2009/04/ball-of-life.jpg" });
                this.Add(new ImageData { Caption = "Apartment Life", ImageURL = "http://static.dezeen.com/uploads/2009/02/squwesterdok-apartment-building-by-mvrdv-westerdok-001.jpg" });
                this.Add(new ImageData { Caption = "Concepts", ImageURL = "http://blog.cleveland.com/andone/2008/07/noname.jpeg" });
                this.Add(new ImageData { Caption = "Going Green", ImageURL = "http://media-cdn.tripadvisor.com/media/photo-s/00/12/6c/df/modern-building.jpg" });
                this.Add(new ImageData { Caption = "Bell V22", ImageURL = "http://image.dieselpowermag.com/f/35405535/1101dp_06_o+1101dp_bell_helicopter_and_boeing_v22_osprey+vertical_grassfield_shot.jpg" });
                this.Add(new ImageData { Caption = "Bell 429", ImageURL = "http://cdn-www.airliners.net/aviation-photos/photos/1/0/3/1270301.jpg" });
                this.Add(new ImageData { Caption = "Bell Helicopter", ImageURL = "http://cdn-www.airliners.net/aviation-photos/photos/4/2/2/1212224.jpg" });
                this.Add(new ImageData { Caption = "Freedom", ImageURL = "http://3.bp.blogspot.com/_2KHnJxC2IFM/TIZggfJVqWI/AAAAAAAABN0/hRkzNaxeW0g/s1600/j0435894.jpg" });
                this.Add(new ImageData { Caption = "Lamborghini", ImageURL = "http://downloadsoftwarestore.com/software_images/25/28/00052825/Hot_Exotic_Cars_II-screenshot.jpg" });
                this.Add(new ImageData { Caption = "Audi", ImageURL = "http://images2.fanpop.com/images/photos/4200000/audi-cars-audi-4294882-1280-960.jpg" });
                this.Add(new ImageData { Caption = "Concept Corvette", ImageURL = "http://www.road4road.com/wp-content/uploads/2010/12/2011-Chevrolet-Corvette-Z06X-Track-Car-Concept-1280x960-Wallpaper.jpg" });
                this.Add(new ImageData { Caption = "Transformer", ImageURL = "http://www.screensaverbase.com/screenshots/images/33_transformers_2_-_optimus_prime_screensaver_screensaver.jpg" });
            }
        }

		// This is my method to load data dynamically from SharePoint. I decided to use an interface so it could be easily replaced if necessary.
        public void LoadData(IImageCollectionDataLoader loader)
        {
            if (!ImageDataCollection.IsInDesignMode)
            {
                loader.LoadData(this);
            }
        }

        private ImageData _Current;
        public ImageData SelectedItem
        {
            get
            {
                return _Current;
            }
            set
            {
                if (_Current != value)
                {
                    _Current = value;
                    OnPropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
                }
            }
        }
    }

So far, the project has been pretty painless. As I mention in the comments of the source code above, I decided to make use of an interface for my data access layer. It is a very simple implementation on purpose — I just want to get things working!

public interface IImageCollectionDataLoader
{
	void LoadData(ImageDataCollection dataContainer);
}

Now I’m ready to build a data access layer. This will make use of the new SharePoint 2010 Client Access Object Model to obtain the data and notify the UI that the data is ready to display. The class will inherit from the interface that was defined above.

    public class SharePointImageLoader : IImageCollectionDataLoader
    {
        public SharePointImageLoader(string LibraryName, int ItemsToLoad)
        {
            _libraryName = LibraryName;
            _totalItems = ItemsToLoad;
        }

        public void LoadData(ImageDataCollection dataContainer)
        {
			Throw New NotImplementedException();
        }
    }

There are some variables that must first be defined so I can have a global access to them. This is because the methods I’ll be using deal are executed asynchronously.

		// The client object model context
        private ClientContext _context = null;
		// A reference to the current website
        private Web _web = null;
		// A reference to the targeted list
        private List _targetList = null;
		// A collection of items queried from the targeted list
        private ListItemCollection _items = null;
		// A reference to the image data collection used for binding on the UI thread -- my view model
        private ImageDataCollection _imagedata = null;
		// The name of the library that will be configured on the web part
        private string _libraryName = string.Empty;
		// The total items to query -- we don't want them all!
        private int _totalItems = 0;

The LoadData method needs an implementation. This will make use of the client object model (humor me, I’m forcing myself to learn how to use it so I can hopefully cheer when using WCF).

        public void LoadData(ImageDataCollection dataContainer)
        {
			// Set the view model -- it will be used later for adding new elements from the query
            _imagedata = dataContainer;
			// Get the current client context for client side queries of SharePoint data
            _context = ClientContext.Current;
			// Get the current web
            _web = _context.Web;
			// Get the list as specified by the name of the library -- this is set in the constructor
            _targetList = _web.Lists.GetByTitle(_libraryName);

			// Set the properties to load, and load the website
            _context.Load(_web, website => website.Title);
			// Load the list
            _context.Load(_targetList);

			// Create a basic query to limit the total items returned
            CamlQuery query = new CamlQuery();
            query.ViewXml =
            @"<View>
                <RowLimit>" + _totalItems + @"</RowLimit>
              </View>";

			// Instruct the context to execute a query to the targeted list and get back the resulting items.
            _items = _targetList.GetItems(query);
			// Tell the context what fields to load
            _context.Load(_items, items => items.Include(item => item.Id, item => item.DisplayName, item => item["Title"], item => item["NameOrTitle"], item => item["EncodedAbsUrl"], item => item["_Comments"], item => item["EncodedAbsThumbnailUrl"]));

			// Now, get busy and load that data!
            _context.ExecuteQueryAsync(AddItemsToCollection, AddItemsFailed);
        }
    }

Loading the data happens asynchrounously, so delegates must be provided for when the method returns successfully or in a failure. This is where I found my first difficulty with the ObservableCollection I used as the base for my View Model. For some reason, the call to add an item to the collection results in a cross-thread security exception — no problem, let’s make sure the call is dispatched to the UI thread properly

        private void AddItemsToCollection(object sender, ClientRequestSucceededEventArgs e)
        {
			// Time to iterate through all the items we've found
            foreach (ListItem listItem in _items)
            {
				// Create a new model
                ImageData newItem = new ImageData();

				// Start setting the properties of the new model -- use the Title if it is available, otherwise
				// the name of the file will work.
                if (listItem["Title"].ToString().Trim().Length > 0)
                {
                    newItem.Caption = listItem["Title"].ToString();
                }
                else
                {
                    newItem.Caption = listItem["NameOrTitle"].ToString();
                }

				// Get the absolute URL
                newItem.ImageURL = listItem["EncodedAbsUrl"].ToString();

				// Load up any comments -- otherwise set it to an empty value.
                if (listItem["_Comments"] != null)
                {
                    newItem.Description = listItem["_Comments"].ToString();
                }
                else
                {
                    newItem.Description = string.Empty;
                }

				// Aha! Here's where we have to make sure we dispatch the add method to the proper thread
				// so let's just do that now
                Deployment.Current.Dispatcher.BeginInvoke(() =>
                    {
                        _imagedata.Add(newItem);
                    });
            }
        }

		// I'm optimistic -- we won't have any errors! That's how confident I am that
		// my code will work -> or maybe, I'm just not interested in making my example overly
		// complex
        private void AddItemsFailed(object sender, ClientRequestFailedEventArgs e)
        {
        }

The data layer can now retrieve data from SharePoint using the client object model and add new models to my view model. Now, there is a little code that I need to add to my code-behind (MVVM Purists, just skip this section and pretend I didn’t say anything about putting something in the code behind). A few extra lines of code are needed to begin loading the data once the Silverlight application is loaded.

        public MainPage(string LibraryName, int ItemsToLoad)
        {
            InitializeComponent();
            SharePointImageLoader loader = new SharePointImageLoader(LibraryName, ItemsToLoad);
            ((ImageDataCollection)LayoutRoot.DataContext).LoadData(loader);
        }

And a modification to the App.xaml.cs file and the Silverlight project is finished:

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            string parameterName = e.InitParams["Name"];
            int totalItems = Convert.ToInt32(e.InitParams["TotalItems"]);
            this.RootVisual = new MainPage(parameterName, totalItems);
            ApplicationContext.Init(e.InitParams, System.Threading.SynchronizationContext.Current);
        }

To make this solution as easy to deploy as possible, I created a SharePoint project and followed the instructions of many blogs on how to reference my XAP file. In order to keep the project as portable as possible, I decided it should be a sandbox solution. The solution is completed after I add a new web part to the SharePoint solution:

using System;
using System.ComponentModel;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;

namespace SilverlightViewer.SilverlightViewerWebPart
{
    [ToolboxItemAttribute(false)]
    public class SilverlightViewerWebPart : WebPart
    {
        private string _ListName = string.Empty;
        [WebBrowsable(true), WebDescription("The name of the list to load images from"), WebDisplayName("Asset List"), Category("Configuration"), Personalizable( PersonalizationScope.Shared)]
        public string ListName
        {
            get
            {
                return _ListName;
            }
            set
            {
                _ListName = value;
            }
        }

        private int _TotalImages = 20;
        [WebBrowsable(true), WebDescription("The total number of images to retrieve from the library"), WebDisplayName("Total Images"), Category("Configuration"), Personalizable(PersonalizationScope.Shared)]
        public int TotalImages
        {
            get
            {
                return _TotalImages;
            }
            set
            {
                _TotalImages = value;
            }
        }

        protected override void OnPreRender(EventArgs e)
        {
            if (ListName.Trim() != string.Empty)
            {
                System.Text.StringBuilder builder = new System.Text.StringBuilder();
                builder.Append("<div id=\"silverlightControlHost\">");
                builder.Append("<object data=\"data:application/x-silverlight-2,\" type=\"application/x-silverlight-2\" width=\"99%\" height=\"99%\">");
                builder.Append("<param name=\"source\" value=\"/_catalogs/wp/Tribridge.Silverlight.ImageViewer.xap\"/>");
                builder.Append("<param name=\"background\" value=\"white\"/>");
                builder.Append("<param name=\"minRuntimeVersion\" value=\"4.0.50826.0\"/>");
                builder.Append("<param name=\"autoUpgrade\" value=\"true\"/>");
                builder.Append("<param name=\"initParams\" value=\"Name=" + ListName + ",TotalItems=" + TotalImages.ToString() + ",MS.SP.url=" + SPContext.Current.Site.Url + "\" />");
                builder.Append("<a href=\"http://go.microsoft.com/fwlink/?LinkID=149156&v=4.0.50826.0\" style=\"text-decoration:none\">");
                builder.Append("<img src=\"http://go.microsoft.com/fwlink/?LinkId=161376\" alt=\"Get Microsoft Silverlight\" style=\"border-style:none\"/>");
                builder.Append("</a>");
                builder.Append("</object><iframe id=\"_sl_historyFrame\" style=\"visibility:hidden;height:0px;width:0px;border:0px\"></iframe></div>");

                Controls.Add(new LiteralControl(builder.ToString()));
            }
            else
            {
                Controls.Add(new LiteralControl("<p>Unable to load the viewer because the list name has not been configured.</p>"));
            }
        }
    }
}

Here you can see the final project inside of SharePoint (however, it takes a long time to load because I decided to upload unprocessed images into my image library — each one is at least 5MB and it takes forever to download all that data!):

About these ads

Author: Chris Quick

I have been a developer of web based solutions since early 2001 delivering solutions to a wide array of organizations using ASP, ASP.NET and SharePoint. I was introduced to SharePoint in 2003 when the consulting firm I worked for at the time introduced it into the workplace. I began working with MOSS 2007 as soon as Microsoft released the RTM version in November 2006. The platform was implemented at the organization I worked for in 2007 and went live in March of that year. I was tasked with the administration and ongoing development of the platform. I currently work as a SharePoint Architect with Artis Consulting, developing solutions for a wide variety of business problems. The goal of this blog is to share my discoveries developing solutions with SharePoint. I welcome your comments and feedback to any post -- and I welcome suggestions for future topics.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 493 other followers