Welcome to the second part of creating an iTunes-inspired Wallview. In this part we are going to write the whole code in C# for the two UserControls that we created with Expression Blend in the previous tutorial.
Final Result Preview
Take another look at the video demo of final result we will be working towards (or just check out the web based demo above):
Step 1: Open the Project
Open Visual Studio and click on "File" > "Open" > "Project/Solution" in the menu bar.
Then browse to the project we created in the first part of this tutorial. We called it "WallviewApp".
Step 2: Open the C# Documents
On the right side of Visual Studio is the Solution Explorer by default. Here you can see every file from the project. Go ahead and open the two *.cs files "image.xaml.cs" and "wallview-img.xaml.cs" from the two UserControls we created in the previous tutorial in Blend as well as the "MainPage.xaml.cs" which got provided automatically.
Once you have opened these three files you can see that all three classes are pretty empty.
Step 3: image Class Declare Variables
Let's start with coding the image class. Make sure you are currently editing the "image.xaml.cs" file and add the following declarations right above the constructor "public image()":
public bool selected; private double defaultSize; private wallview_img wallview; private SolidColorBrush colorDefault, colorSelected;
Step 4: image Class Write the Constructor
Now we are going to program the constructor which has two parameters: a BitmapImage and a String. Furthermore the previous declared variables are initialized, the parameters from the constructor are assigned to the source of the "img" and the text of the "imgName" Textblock. We also register an eventhandler for the MouseLeftButtonDown event of the "img":
public image(BitmapImage src, String name) { // Required to initialize variables InitializeComponent(); selected = false; defaultSize = 200; colorDefault = new SolidColorBrush(Color.FromArgb(0x00, 0x76, 0xA2, 0xF9)); colorSelected = new SolidColorBrush(Color.FromArgb(0xFF, 0x76, 0xA2, 0xF9)); this.img.Source = src; this.imgName.TextAlignment = TextAlignment.Center; this.imgName.Text = name; this.imgDate.TextAlignment = TextAlignment.Center; this.img.MouseLeftButtonDown += new MouseButtonEventHandler(image_MouseLeftButtonDown); }
The parameter BitmapImage is underlined with a red color because Visual Studio can't find the namespace. To fix that just click the word BitmapImage and a small blue rectangle should appear under the letter B:
When you move your mouse over the small rectangle a drop-down menu will appear. Click on the entry "Using System.Windows.Media.Imaging;":
Now Visual Studio knows that namespace, and the red underline will disappear.
Step 5: image Class Code Some Methods
Right after the constructor we are going to write a couple of methods which we will need later. The methods are pretty self descriptive from their method names. image_MouseLeftButtonDown
is the event handler of the MouseLeftButtonDown
event of the "img" that we registered in the constructor. It basically controls what happens when you click the image, depending on its status (whether it is selected already or not):
public void setWallview(wallview_img wv) { wallview = wv; } public void changeSize(double newsize) { this.imgName.Width = defaultSize * newsize; this.imgSize.Width = defaultSize * newsize; this.imgSize.Height = defaultSize * newsize; } public void unselectImage() { selected = false; this.imgBorder.BorderBrush = colorDefault; } public void selectImage() { selected = true; this.imgBorder.BorderBrush = colorSelected; } private void image_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { wallview.imageSelected = true; if (wallview.ctrlPressed == true) { if (selected == true) unselectImage(); else selectImage(); } else { wallview.unselectAllImages(); selectImage(); } e.Handled = true; }
Now we're done with the image class.
Don't worry about the red underlined parts in your code. These variables and methods don't exist yet and that's why Visual Studio doesn't know them but we are going to change that soon.
Step 6: wallview-img Class Declare Variables
Since we are done with the image class open the "wallview-img.xaml.cs" document. At first write the declarations of variables right above the constructor "public wallview_img()":
public List<image> imageList; private List<StackPanel> panelList; public bool ctrlPressed, imageSelected; private double defaultSize, changeSize;
Just like before with the BitmapImage Visual Studio doesn't know the namespace of List
. To fix this, as before, click one of the Lists, then on the blue rectangle and select "using System.Collections.Generic;" from the menu:
Step 7: wallview-img Class Code the Constructor
Add the following code which initializes the previously declared variables, registers some events and disables the buttons in the navigation bar:
public wallview_img() { // Required to initialize variables InitializeComponent(); ctrlPressed = false; imageSelected = false; imageList = new List<image>(); panelList = new List<StackPanel>(); defaultSize = 200; changeSize = 1; //registering all the events MouseLeftButtonUp += new MouseButtonEventHandler(wallviewMouseLeftButtonUp); SizeChanged += new SizeChangedEventHandler(resizeScrollViewer); KeyDown += new KeyEventHandler(keyDownEvent); KeyUp += new KeyEventHandler(keyUpEvent); this.slider.ValueChanged += new RoutedPropertyChangedEventHandler<double>(sliderValueChanged); //buttons we dont need yet this.btnAllAlbums.IsEnabled = false; this.btnCurrentAlbum.IsEnabled = false; this.btnNext.IsEnabled = false; this.btnPrev.IsEnabled = false; }
Step 8: wallview-img Class Write the Methods
Add the following methods below the constructor. The comment before each method explains what the method is doing:
//adds an image to the imagelist and calls resizeimages() which basically adds it to the last stackpanel public void addImage(image img) { img.imgName.Width = 200; img.setWallview(this); imageList.Add(img); resizeImages(); } //clears the whole drawn content, every panel, albumlist and panellist public void clearLists() { imageList.Clear(); foreach (StackPanel x in panelList) { x.Children.Clear(); } this.content.Children.Clear(); panelList.Clear(); } //calculates how many stackpanels = rows are needed public void updatePanels() { if (imageList.Count > 0) { foreach (StackPanel sp in panelList) sp.Children.Clear(); panelList.Clear(); double gridWidth = 0; if (this.content.ActualWidth == 0) gridWidth = 800; else gridWidth = this.content.ActualWidth; int gridWidthInt = Convert.ToInt32(gridWidth); int imageAmount = imageList.Count; int imageMargin = 10; int imageWidth = Convert.ToInt32(defaultSize * changeSize); int imageSize = imageWidth + 2 * imageMargin; double ratio = gridWidth / (double)imageSize; int ratioInt = Convert.ToInt32(ratio); if (ratioInt - ratio > 0) ratioInt -= 1; int newImageMargin = ((gridWidthInt - ratioInt * imageWidth) / ratioInt) / 2; double panelAmountDouble = (double)imageAmount / ratioInt; int panelAmountInt = (int)panelAmountDouble; if (panelAmountDouble - panelAmountInt > 0) panelAmountInt++; if (panelAmountInt < 1) panelAmountInt = 1; int x = 0; for (int i = 0; i < panelAmountInt; i++) { StackPanel panel = new StackPanel(); panel.Orientation = Orientation.Horizontal; panel.Margin = new Thickness(0, 5, 0, 0); for (int j = 0; j < ratioInt; j++) { if (x < imageAmount) { imageList[x].Margin = new Thickness(newImageMargin, 0, newImageMargin, 10); imageList[x].changeSize(changeSize); imageList[x].setWallview(this); panel.Children.Add(imageList[x]); } x++; } panelList.Add(panel); } } } //selects all images, gets called when ctrl + a is pressed public void selectAllImages() { foreach (image i in imageList) i.selectImage(); } //unselects all iamges public void unselectAllImages() { foreach (image i in imageList) i.unselectImage(); } //gets called when the slider value changes private void resizeImages() { updatePanels(); this.content.Children.Clear(); foreach (StackPanel sp in panelList) this.content.Children.Add(sp); } //method gets called by the slidervaluechanged event public void changeImageSize(double newsize) { changeSize = newsize; resizeImages(); }
Step 9: wallview-img Class Eventhandlers
In this step we write the needed event handlers for the events we registered in the constructor earlier:
//eventhandler for when the slider gets changed private void sliderValueChanged(object sender, System.Windows.RoutedPropertyChangedEventArgs<double> e) { changeImageSize(this.slider.Value); } //eventhandler that gets called when the windowsize changes private void resizeScrollViewer(object sender, System.Windows.SizeChangedEventArgs e) { resizeImages(); } //eventhandler that unselects all images when you dont click an image void wallviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (!imageSelected) unselectAllImages(); else imageSelected = false; } //eventhandler for pressing keys private void keyDownEvent(object sender, System.Windows.Input.KeyEventArgs e) { if (e.Key == Key.Ctrl) ctrlPressed = true; else if (e.Key == Key.A) if (ctrlPressed) selectAllImages(); } //eventhandler for releasing keys private void keyUpEvent(object sender, System.Windows.Input.KeyEventArgs e) { if (e.Key == Key.Ctrl) ctrlPressed = false; }
Now we are done with the wallview-img
class. Let's continue with creating a webservice which we will need for the MainPage
class.
Step 10: Create a Webservice
The webservice that we are going to write basically provides us with the images from a certain folder. To create a webservice right-click on "WallviewApp.Web" in the Solution Explorer on the right side of Visual Studio and select "Add" > "New Item" from the menu:
From the popup select "Silverlight-enabled WCF Service Visual C#" and enter "WCF.svc" for a name, then click "Add":
As you can see we obtained another class called WCF with its own code document, "WCF.svc.cs".
Step 11: WCF Class Code Two Methods
Add the following two methods in the WCF class right beneath the line that says "// Add more operations here and mark them with [OperationContract]
":
//method for getting all the file names within a folder [OperationContract] public string[] getFileNames(String dir) { try { String tmp = HttpContext.Current.Request.MapPath(dir); return Directory.GetFiles(@tmp, "*.jpg"); } catch (Exception) { return null; } } //method that returns the date of creation of a file or folder [OperationContract] public String getFileDates(String file, int i) { return i.ToString() + "-" + File.GetLastWriteTime(file).ToString(); }
Fix the missing namespaces like we did earlier by clicking into the affected names, then on the blue rectangle and on "import ..." or by adding these two lines manually at the top of the document:
using System.Web; using System.IO;
Step 12: Service Reference Part 1
In order to be able to use the webservice we need to add a Service Reference to the main project. Before we can successfully do this we have to build the project once. Therefore click on "Build" from the menu at the very top of Visual Studio and after that on "Build WallviewApp":
Step 13: Service Reference Part 2
After the build succeeds, right-click on "WallviewApp" on the right side in the Solution Explorer and choose "Add Service Reference" from the menu:
Step 14: Service Reference Part 3
In the upcoming popup click on the button "Discover" and enter "WCFRef" in the Namespace field, then click "OK":
Without building the project before trying to add a Service Reference you would have gotten this error message:
Step 15: MainPage Class Declare Variables
Open the "MainPage.xaml.cs" file and add the following lines of code above the constructor "public MainPage()
":
private WCFRef.WCFClient client; private string imagedir = "/image/"; public wallview_img wvi; private int amountImages;
Step 16: MainPage Class Program the Constructor
The constructor of the MainPage
looks like this. We initialize the declarations from the previous step, register the webservice's eventhandlers and add the wallview_img
named "wvi
" to the Grid
"LayoutRoot
" of the MainPage
:
public MainPage() { InitializeComponent(); amountImages = 0; client = new WallviewApp.WCFRef.WCFClient(); client.getFileNamesCompleted += new EventHandler<WallviewApp.WCFRef.getFileNamesCompletedEventArgs>(client_getFileNamesCompleted); client.getFileNamesAsync(imagedir); client.getFileDatesCompleted += new EventHandler<WallviewApp.WCFRef.getFileDatesCompletedEventArgs>(client_getFileDatesCompleted); wvi = new wallview_img(); this.LayoutRoot.Children.Add(wvi); }
Step 17: Code the Eventhandlers for the Webservice
Add the following two eventhandlers and the method below the constructor of the MainPage.
The "client_getFileNamesCompleted()
" gets an array of strings which get returned by the webservice. The array is converted into separate strings from which the filename is extracted.
Using the localhost
address, the port, the image directory, and filename, we build a Uri
called "src". That Uri
is used to create a new BitmapImage
"bmi" which is needed to create a new image "tmp". The image "tmp" then gets added to the wallview_img
"wmi".
Afterwards, the method that returns the creation date of a file is called together with a counter number. Whatever that method returns gets processed by the handler "client_getFileDatesCompleted()". Since a string in the format >2-18.02.2009 12:32:23 is returned from the webservice, we have to split off the counter number at the beginning and the date in the middle.
When this procedure is completed the final date looks like >18.02.2009 and is assigned to the
Textblock
"imgDate" of the corresponding image.
//event handler for getting the filenames from of the folder private void client_getFileNamesCompleted(object sender, WallviewApp.WCFRef.getFileNamesCompletedEventArgs e) { if (e.Result != null) { foreach (string s in e.Result) { int pos = s.LastIndexOf("\\"); string filename = s.Substring(pos + 1); int port = Application.Current.Host.Source.Port; Uri src = new Uri("http://localhost:" + port + imagedir + filename); BitmapImage bmi = new BitmapImage(src); image tmp = new image(bmi, filename); this.wvi.addImage(tmp); amountImages++; getFileDate(s, amountImages - 1); } } else { MessageBox.Show("returned null in files completed"); } } //method that calls the webservice asynch with a filepath string and a number so we can assing the returned date string to a certain image again private void getFileDate(String s, int i) { this.client.getFileDatesAsync(s, i); } //event handler for getting the filedates private void client_getFileDatesCompleted(object sender, WallviewApp.WCFRef.getFileDatesCompletedEventArgs e) { if (e.Result != null) { String dt = e.Result; int number = Convert.ToInt32(dt.Remove(dt.LastIndexOf("-"))); String date = dt.Remove(dt.LastIndexOf(" ")); if (number < 10) date = date.Remove(0, 2); else if (number < 100) date = date.Remove(0, 3); else date = date.Remove(0, 4); this.wvi.imageList[number].imgDate.Text = date; } else { MessageBox.Show("returned null in dates completed"); } }
As in the other source files the namespace "BitmapImage" cannot be found. To repair this click the blue rectangle and import it after you clicked into the text of BitmapImage
, or add the following line to the top of the document manually:
using System.Windows.Media.Imaging;
Step 18: Run the Project
Go ahead and execute the project to see if everything works. You can do this by either pressing "F5" on your keyboard, by clicking the button with a typical "Play" icon below the menu at the top on the icon bar, or by selecting the entry "Start Debugging" inside the "Debug" submenu from the menu at the very top of Visual Studio:
Your browser opens and you will get this error message:
What's the problem?
We told the webservice to check the folder "http://localhost:port/image/" for *.jpg files and obviously neither that folder nor any images in that folder exist yet.
Step 19: Prepare the Image Folder
Navigate to the project directory with your Windows Explorer. For me it is the following path:
Open the folder "WallviewApp.Web" and create a new folder called "image" inside of it.
Now open the folder "image" and paste some *.jpg images into it.
Step 20: Run the Project Again
Once you placed a couple of images inside the image folder either press refresh inside your webbrowser (if it is still open) or simply run the project again by pressing the "F5" key in Visual Studio. As a result you should see our final Wallview:
You can influence the size of the displayed images with the slider in the navigation bar.
Conclusion
We are done for now with this tutorial and i hope you enjoyed it and learned something, too.
The approximate total time it took me to develop this from scratch was about 20 hours. The wallview for albums which you can see in the video below took me about 15 hours and another 10 hours to combine both wallview types.
You might ask yourself what the point of being able to select one or more images is. Right now that functionality is useless but I could imagine adding the possibility to start a slideshow or create a playlist from the selected images for example. And if you are wondering why we added the buttons "All Albums", "Current Album", "<" and ">" in the navigation bar but never used them...
My intention is to develop another tutorial that automatically generates photo albums based on the filenames of the images. Of course that tutorial will extend the project from the tutorial you just completed here. I'd definitely like to do that if it is popular with the audience. To get an impression of what the wallview for images that we just created would look like in combination with a wallview for photo albums take a look at the following video:
For any comments, suggestions or concerns, please leave a note in the comment section. Thanks for reading!
Comments