Monday, January 11, 2010

Silverlight, RIA Services, and the Reactive Framework (Rx)

This post is not specific to RIA Services but I thought I'd add it to the title since the example is based on RIA Services.

Anyone who is familiar with RIA Services will recognize this piece of code that is used to load a entity -
EntityQuery<Person> personQuery = DomainContext.GetPersonQuery();
DomainContext.Load(personQuery, loadOperation =>
{
    HandleLoadOperationError(loadOperation);
}, null);
Typically this piece of code wll be called from some other code in a simulated synchronous fashion, like this -
public void LoadPerson(Action callback)
{
    EntityQuery<Person> personQuery = DomainContext.GetPersonQuery();
    DomainContext.Load(personQuery, loadOperation =>
    {
        if (!HandleLoadOperationError(loadOperation))
            return;

        if (callback)
        {
            InitializeData();
            callback(null);
        }
    }, null);
}

public void DoMore()
{
    ShowBusyIndicator(); // Disable the active window and show busy indicator
    LoadPerson(() =>
    {
        // This will be called after the asychronous load in LoadPerson has completed
        // ...
        CloseBusyIndicator(); // Re-enable user interaction
        // ...
    }    
}
Let's say we have to display a search page with a bunch of fields, mostly pre-populated combo boxes and list boxes. The data for the combobox and listbox comes from corresponding Entity objects. One way to load the Entities could be the following -
public void LoadLookupEntities(Action callback)
{
    DomainContext.Load(DomainContext.GetPersonQuery(), loadOperation1 =>
    {
        if (!HandleLoadOperationError(loadOperation1))
            return;

        DomainContext.Load(DomainContext.GetGenderQuery(), loadOperation2 =>
        {
            if (!HandleLoadOperationError(loadOperation2))
                return;

            DomainContext.Load(DomainContext.GetEthnicityQuery(), loadOperation3 =>
            {
                if (!HandleLoadOperationError(loadOperation3))
                    return;

                // and so on ...

                if (callback)
                {
                    InitializeData();
                    callback(null);
                }
            }, null);
        }, null);
    }, null);
}
Now let's go ahead and refactor LoadLookupEntities with the Reactive Framework (Rx). First we need to create an extension method LoadAsync on DomainContext which is essentially the same as Load but will return an IOBservable<LoadOperation<>> instead of LoadOperation<>. This is one way to do it (this technique can be used for async patterns that don't follow the begin/end pattern) -
public static class DomainContextExtensions
{
    public static IObservable<LoadOperation<TEntity>> LoadAsync<TEntity>(
        this DomainContext domainContext,
        EntityQuery<TEntity> query)
        where TEntity : Entity
    {
        var asyncSubject = new AsyncSubject<LoadOperation<TEntity>>();

        domainContext.Load(query, loadOperation =>
        {
            if (loadOperation.HasError)
                asyncSubject.OnError(loadOperation.Error);
            else
            {
                asyncSubject.OnNext(loadOperation);
                asyncSubject.OnCompleted();
            }
        }, null);

        return asyncSubject;
    }
}
Once we have the extension method let's go ahead and use it.
public void LoadLookupEntities(Action callback)
{
    var loadAll =
        from loadOperation1 in DomainContext.LoadAsync(DomainContext.GetPersonQuery())
        where !HandleLoadOperationError(loadOperation1)
        from loadOperation2 in DomainContext.LoadAsync(DomainContext.GetGenderQuery())
        where !HandleLoadOperationError(loadOperation2)
        from loadOperation3 in DomainContext.LoadAsync(DomainContext.GetEthnicityQuery())
        where !HandleLoadOperationError(loadOperation3)
        // ...
        select true;

    loadAll.Subscribe(loadedAll =>
    {
        if (loadedAll)
        {
            InitializeData();
            callback(null);
        }
    });
}
Or if we want parallel execution -
var loadAllInParallel =
    Observable.ForkJoin<OperationBase>(
        DomainContext.LoadAsync(DomainContext.GetPersonQuery()).Select(s => s as OperationBase),
        DomainContext.LoadAsync(DomainContext.GetGenderQuery()).Select(s => s as OperationBase),
        DomainContext.LoadAsync(DomainContext.GetEthnicityQuery()).Select(s => s as OperationBase),
    ).Finally(() => System.Diagnostics.Debug.WriteLine("Done!"));

loadAllInParallel.Subscribe(
    loadOperationsAll =>
    {
        InitializeData();
        callback(null);
    },
    error =>
    {
        // Handle Errors
    });
That's it!

Why would you want to do this? If you don't know the answer, go ahead and check out the resources listed below. If you still don't know the answer, you don't need it!

My reaction to the Rx framework - Unfreaking believably cool! More importantly, it's so useful that I suspect I will be using it as commonly as I do Linq.

Note that the examples above are just that - examples. Obviously the error handling has a lot to be desired!

Some Really Good Resources on Rx

Monday, October 19, 2009

Remove all empty XML elements using LINQ

While working on the XamlWriter I had to do some post processing on the generated Xaml. One was to delete all nodes that did not have any attributes or contain any elements. The first pass of removing the empty nodes could generate more empty nodes and hence this problem is recursive in nature.

This was my first attempt
var query = doc.Descendants().Where(c => !c.HasAttributes && c.IsEmpty);
while (query.Count())
   query.Remove();

This could be certainly improved using the .Any operator

var query = doc.Descendants().Where(c => !c.HasAttributes && c.IsEmpty);
while (query.Any())
   query.Remove();

The problem with the above solution(s) is that the query is executed twice, once for the Any/Count method and once for the Remove method. The next approach provided 2x performance gains (in the best case scenario) over the above approach

void RemoveEmptyNodes(XElement root)
{
   var query = root.Descendants().Where(e => !e.HasAttributes && e.IsEmpty).ToList();
   query.Remove();
   if (query.Any())
      RemoveEmptyNodes(root);
}

The reason for the performance improvement is that although both the Any and the Remove operations are executed on the query, the .ToList operator executes and caches the result and hence the query is executed only once.

The concern I have with the above recursive solution is the memory requirement for the local variable query which is of type IEnumerable.

Maybe something like this will help but I don't understand GC (garbage collection) in .NET well enough to recommend this approach

void RemoveEmptyNodes(XElement root)
{
   var query = root.Descendants().Where(e => !e.HasAttributes && e.IsEmpty).ToList();
   query.Remove();
   if (query.Any())
   {
      query = null;
      RemoveEmptyNodes(root);
   }
}

Anyone with better or alternate approaches please post your thoughts as comments.

Friday, October 16, 2009

Silverlight 3 XamlWriter - v0.4 - DataTemplate support

The new version of XamlWriter with DataTemplate support is now available. Check the previous posts for download and usage information.

While it is possible to recreate the DataTemplate it is not possible to recreate ContentTemplate. Hence I couldn't add support for ContentTemplate. There is also support for the Style class but there is an issue with it.

The DataTemplate example

Thursday, October 15, 2009

Silverlight 3 XamlWriter - v0.3 - Storyboard and TransformGroup support

An update every day - Wow! Thought I'd keep the momentum going while I have a liiittle free time. Once it goes on the back-burner, it will lie there to bite the dust!

If anyone has tried the XamlWriter (which I am sure no one has, yet!) it would be clear that it doesn't work for Storyboards and TransformGroups. That is because they are a slightly special case. Why? I'll leave that for some future post. Anyways it's fixed now.

Please check out my previous post for download and usage information.

I also thought I'd take this opportunity to point out some issue that I encountered. If you had read my previous posts on this subject you would know that I used reflection to implement the XamlWriter.

The way to retrieve the properties of an object using reflection is like this -
  PropertyInfo[] props = target.GetType().GetProperties();

I now needed to to check if the property is readonly. The reason for this is that a readonly property cannot be used in Xaml. So I went ahead and used the CanWrite property to check if the property could be written to, like this -
  if (!prop.CanWrite)

But, guess what? It doesn't work in all cases. For example, the IsFocussed property of a Button control returns true for CanWrite. I put the IsFocussed property in the Xaml and it threw an error indicating clearly that this was a read only property.

So there had to be a different way. After all the Xaml parser knows more about readonly properties than the CanWrite property. That is when I discovered the GetSetMethod method on ProperyInfo. This worked wonderfully -
  if (prop.GetSetMethod == null)

Whether this is a bug or some explainable feature, I don't know. Maybe I will post this on the Silverlight.net forums and see if there is an explanation. In the meantime, there is a workaround for me to continue making progress!

The Storyboard and TransformGroup example

Wednesday, October 14, 2009

Silverlight 3 XamlWriter - v0.2

I was just going through the Silverlight.net forums to see if there was any interesting question to answer and I came across a post which had some interesting Xaml content. This used the Accordian control and looked like a good test case so I copied the Xaml to my project and invoked XamlWriter on it.

And...it did not work! That was expected though. I would be very surprised if it did work.

So, back to the drawing board - a few fixes here, a few fixes there, discovered a few more bugs, fixed them, and stopped when I got it to work with the second Xaml test case.

Get the newer version of XamlWriter from here - XamlWriter.dll

The second example

Tuesday, October 13, 2009

Silverlight 3 XamlWriter - A basic implementation

I am sure that anyone who has worked extensively with Silverlight has felt the need for a XamlWriter at some time. I mostly need it for debugging purposes - to see the xaml of the dynamically assembled controls.

I have been taking the cumbersome route of stepping through the control hierarcy using the debugger, but enough is enough! Just takes too much time. It probably is quicker to just write some code which takes in a FrameworkElement and outputs the Xaml representation of it.

I did find 3 resources on the web that would prove useful (or not!)
  1. XamlWriter by Mehran (rambler.elf on the Silverlight.net forums) - A nice simple reflection based approach that would prove the starting point for my project
  2. XamlWriter by the SilverlightContrib team - Did not try this since it is embedded deep in another project. Not sure if this wuld work for me but since I like to have more control I decided to write one myself (based on Mehran's code)
  3. Silverlight Spy - A third party tool that could be used to extract the Xaml of a specific control. Not so useful for me since I need to hook it in my code
So I decided to take Mehran's code and make it work on Silverlight 3. Then I refined it and added some features and refined it a bit more and added some more features and...

It's by no means complete or well tested but if you feel brave enough
  • just download this file - XamlWriter.dll 
  • add a reference to this file from your project 
  • and use the following snippet of code to test it

using projectsilverlight.blogspot.com.XamlWriter;

XamlWriter xamlWriter = new XamlWriter();
string xaml = xamlWriter.WriteXaml(MyControl, XamlWriterSettings.LogicalTree);

Note that XamlWriter uses System.Xml.Linq for some processing so you will find this dll getting added to the xap as well.

XamlWriterSettings has 3 values which can be combined to get different output. The 4 valid combinations are

WriteXaml(MyControl, XamlWriterSettings.LogicalTree);
WriteXaml(MyControl, XamlWriterSettings.LogicalTree | XamlWriterSettings.AllAttributes);
WriteXaml(MyControl, XamlWriterSettings.VisualTree);
WriteXaml(MyControl, XamlWriterSettings.VisualTree | XamlWriterSettings.AllAttributes);
Like I mentioned before - this is not well tested. In fact, the example below is the only case I have tested. I am putting it up nevertheless to get some feedback. If you find that it is not working for a specific case, send me the Xaml/code that it is not working for and I will take a look. At some point in the future, if still relevant, I will open source this code. But now it is closed source for various reasons - the code quality sucks, haven't taken permission from Mehran yet, want better control so that a single codebase gets improved upon rather than each individual fixing it for his/her needs.

Enjoy!

An example

Friday, August 14, 2009

I am available for contract work

It's been a long time since I last put up a post on this blog! I kind of got buried in my work which got more hectic since we had a few layoffs, which is when I finally decided to take a break, after 5 long but incredibly exciting years, and spend some time with my 2 year old daughter. I had an absolutely wonderful time the last few months taking it easy, but now the time has come to get back in the game!

I recently peeked into the MSDN magazine (July edition) and noticed this article - 'Taking Silverlight Deep Zoom to the Next Level' - by Jeff Prosise, and suddenly realized how much I missed doing this Deep Zoom stuff. The first half of his article is similar to some of the code I have on this blog. I was kind of surprised by this since I thought that no one would be interested in reading this stuff because it was so old - so last year! :) Apparently people are still interested in Deep Zoom related articles. I corroborated this by looking at the hits this blog has been receiving and I was surprised that the activity in the recent few months has been more than last year. I thought this was hot last year when we got a glimpse of the hard rock site but I guess lots of developers have been recently getting into Silverlight, especially Deep Zoom related.

Anyways, I am free and passively looking for a contract assignment in .Net, Silverlight etc. I enjoy spending time with my daughter and would like to continue doing that so I am not looking for any assignment that includes travel (minimal travel like a day here and there is fine). So if you have an opening which doesn't include travel and think that I could be a good addition to your team (based on what you see on this blog), give me a buzz and we can chat.

Cheers,

Wilfred
contactwilfred@yahoo.com
Los Angeles, CA

Saturday, October 18, 2008

SimpleViewer-SL ported to Silverlight 2 and uploaded to CodePlex

I finally got around to uploading the SimpleViewer-SL source code to CodePlex. Minor changes were required to get the code to compile on Silverlight 2.

The SimpleViewer-SL project on CodePlex can be found here: http://www.codeplex.com/simpleviewerSL. Please feel free to report all bugs/issues/enhancements on this site.

Tuesday, October 14, 2008

DeepZoom sample ported to Silverlight 2

The source code can be found here

For those wanting to download the legacy code (for whatever reason!) --Note that the disclaimer from this post still holds!

All inline samples have been upgraded to work with Silverlight 2. If you are still using Beta 2 you will need to upgrade to view the samples!

Monday, July 7, 2008

Deep Zoom - Rendering in 2 different layouts based on zoom factor

Berend, the author of Generate Silverlight 2 DeepZoom image collection from multi-page tiff, wanted to know if it was possible to render the contents of the deep zoom control in different layouts based on the current zoom factor. Initially, he wanted to show the images in a single row layout with each image taking the entire height of the control. As the user zooms out, after the image reaches a certain factor - say half the height, he wanted to switch to a thumbnails mode where all the images are displayed in a multi row and column format (much like how it is now).

It was pretty easy to do and here is the result of my experimentation:


Try zooming out so that the first image height is less than half the height of the control. After the motion is completed, it should switch to the thumbnails mode. I have currently based this off the height of the first image. The layout will switch depending on whether the height of the first image is greater than or less than half the height of the control.

The code:

enum DisplayMode {
Full = 1,
Thumbnails = 2
}

DisplayMode _displayMode = DisplayMode.Full;

msi.MotionFinished += delegate(object sender, RoutedEventArgs e)
{
// This is required to ensure that ArrangeImages is called only once
if (msi.UseSprings == false) {
ShowAllImages();
msi.UseSprings = true;
}
else {
DisplayMode _origDisplayMode = _displayMode;

Rect imageRectElement = msiLogicalToElementRect(GetSubImageRect(0));
_displayMode = (msi.ActualHeight / imageRectElement.Height < 2) ? DisplayMode.Full : DisplayMode.Thumbnails;

if (_displayMode != _origDisplayMode) {
Reset();
}
}
};

public void Reset()
{
_lastMousePos = new Point(0, 0);
msi.ViewportOrigin = new Point (0, 0);
msi.ViewportWidth = 1.0;

// Show all the images
_imagesToShow.Clear();
_imagesToHide.Clear();
for (int i=0; i<msi.SubImages.Count; i++) {
_imagesToShow.Add(msi.SubImages[i]);
}
ArrangeImages();
}

public void ArrangeImages()
{
InitStoryboard();

double containerAspectRatio = this.msi.ActualWidth / this.msi.ActualHeight;
double spaceBetweenImages = 0.005;

List<SubImage> subImages = new List<SubImage>();
_imagesToShow.ForEach(subImage => subImages.Add(new SubImage(subImage)));

// Capture the total width of all images
double totalImagesWidth = 0.0;
subImages.ForEach(subImage => totalImagesWidth += subImage.Width);

// Calculate the total number of rows required to display all the images
int numRows = (_displayMode == DisplayMode.Thumbnails) ? (int)Math.Sqrt((totalImagesWidth / containerAspectRatio)+1) : 1;

// Assign images to each row
List<Row> rows = new List<Row>(numRows);
for (int i=0; i<numRows; i++)
rows.Add(new Row(spaceBetweenImages));

double widthPerRow = totalImagesWidth / numRows;
double imagesWidth = 0;
// Separate the images into rows. The total width of all images in a row should not exceed widthPerRow
for (int i=0, j=0; i<numRows; i++, imagesWidth=0) {
while (imagesWidth < widthPerRow && j < subImages.Count) {
rows[i].AddImage(subImages[j]);
subImages[j].RowNum = i;
imagesWidth += subImages[j++].Width;
}
}

// At this point in time the subimage height is 1
// If we assume that the total height is also 1 we need to scale the subimages to fit within a total height of 1
// If the total height is 1, the total width is aspectRatio. Hence (aspectRatio)/(total width of all images in a row) is the scaling factor.
// Added later: take into account spacing between images
rows.ForEach(Row => Row.Scale(containerAspectRatio));

// Calculate the total height, with space between images, of the images across all rows
// Also adjust the colNum for each image
double totalImagesHeight = (numRows - 1) * spaceBetweenImages;
rows.ForEach(Row => totalImagesHeight += Row.Height);

// The totalImagesHeight should not exceed 1.
// if it does, we need to scale all images by a factor of (1 / totalImagesHeight)
if (((_displayMode == DisplayMode.Thumbnails && totalImagesHeight > 1)) || _displayMode == DisplayMode.Full) {
subImages.ForEach(subImage => subImage.Scale(1 / (totalImagesHeight+spaceBetweenImages)));
totalImagesHeight = (numRows - 1) * spaceBetweenImages;
rows.ForEach(Row => totalImagesHeight += Row.Height);
Debug.Assert(totalImagesHeight <= 1);
}

// Calculate the top and bottom margin
double margin = (1 - totalImagesHeight) / 2;

// First hide all the images that should not be displayed
_imagesToHide.ForEach(subImage => subImage.Opacity = 0.0);

// Then display the displayable images to scale
for (int i=0; i<_imagesToShow.Count; i++) {
double X = rows[subImages[i].RowNum].CalcX(subImages[i].ColNum);
double Y = margin;
for (int j=0; j<subImages[i].RowNum; j++)
Y += spaceBetweenImages + rows[j].Height;

_imagesToShow[i].ViewportWidth = containerAspectRatio / subImages[i].Width;
AnimateImage(_imagesToShow[i], new Point(-(X / subImages[i].Width), -(Y / subImages[i].Width))); // for animation, use this statement instead of the next one
_imagesToShow[i].Opacity = 1.0;
}
// Play Storyboard
_moveStoryboard.Begin();
}

Thursday, July 3, 2008

I'm honored!

I just noticed that my blog is referenced in the MSDN Silverlight Beta 2 documentation, specifically http://msdn.microsoft.com/en-us/library/system.windows.controls.multiscaleimage(VS.95).aspx (search for Filter Example). This is definitely an honor!

Does this mean that I can now ask Microsoft for a free copy of Visual Studio 2008? Just kidding :) I am currently working on a Silverlight project so I have access to VS2008. I still use Notepad++ on my laptop though, since it is underpowered for VS.

I finally got around to uploading the source code for the DeepZoom sample. You will find the link to the code in this post - DeepZoom sample ported to Silverlight 2 Beta 2

Tuesday, July 1, 2008

Deep Zoom Composer source code

Not really...but pretty close! Check out Berend's article on CodeProject -
Generate Silverlight 2 DeepZoom image collection from multi-page tiff

I haven't tried it myself so I don't know if it works, but it definitely opens up a lot of possibilities - Dynamic gallery generation; Adding images to collection without regenerating all the tiles...

If only Microsoft would open source the DeepZoom Composer code, or at least the image/tile generation part!

Great work, Berend Engelbrecht!

Monday, June 30, 2008

SimpleViewer-SL ported to Silverlight 2 Beta 2

SimpleViewer-SL now works on Beta 2 as well. I updated the inline samples in the previous posts so folks with Beta 2 should be able to view all the sample galleries that I created using SimpleViewer-SL.

If you recollect from my earlier post, I did not use the ScrollViewer control since I wanted to keep the .xap package as small as possible. This is no longer the case since ScrollViewer is now part of the Silverlight framework itself. I still haven't updated the code to take advantage of this but might do so at a later date.

Sunday, June 29, 2008

DeepZoom sample ported to Silverlight 2 Beta 2

I have been planning to do this for some time now but finally managed to squeeze some time for this activity. I am going to leave the previous posts as-is so that the folks with Beta 1 can look at the previous sample.



It was a relatively straightforward exercise but one thing I noticed is that msi.Width/Height doesn't work properly in Beta 2, but luckily msi.ActualWidth/ActualHeight works! I also used the new tagging format (Metadata.xml) that the latest DeepZoom Composer generates.

I will post the source code in the next post.
The source code has been uploaded and can be found here. Note that the disclaimer from this post still holds!

Monday, May 12, 2008

Scrolling, without using ScrollViewer!

The main objective when building SimpleViewer-SL was to keep the executable as small as possible and hence I didn't have the option of using ScrollViewer. I still needed scrolling functionality though and hence had to take some alternate approach. This post is a result of my findings. There is possibly no reason for anyone to take this approach, unless there is a compelling reason to not use ScrollViewer, but it might prove useful!

So how did I go about implementing scrolling? The trick is to contain the visible area and use TranslateTransform.

Let's look at the Xaml first
<UserControl x:Class="SimpleViewer_SL.Thumbnails"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid x:Name="LayoutThumbnailsViewport" HorizontalAlignment="Left">
<Grid x:Name="LayoutThumbnails" HorizontalAlignment="Left">
<Grid.RenderTransform>
<TranslateTransform x:Name="GridTranslate" />
</Grid.RenderTransform>
<Grid.Resources>
<Storyboard x:Name="GridScrollStoryboard">
<DoubleAnimation x:Name="GridScrollAnimation" Storyboard.TargetName="GridTranslate" Storyboard.TargetProperty="X" Duration="00:00:00.5"/>
</Storyboard>
</Grid.Resources>
</Grid>
</Grid>
<Canvas x:Name="RightNav" Height="25" Width="30" Background="Transparent" HorizontalAlignment="Right">
<Path Height="25" Width="30" Stretch="Fill"
Data="F1 M 0,50L 130,50L 80,0L 130,0L 200,70L 130,140L 80,140L 130,90L 0,90L 0,50 Z">
</Path>
</Canvas>
<Canvas x:Name="LeftNav" Height="25" Width="30" Background="Transparent" HorizontalAlignment="Left">
<Path Height="25" Width="30" Stretch="Fill"
Data="F1 M 200,90L 70,90L 120,140L 70,140L 0,70L 70,0L 120,0L 70,50L 200,50L 200,90 Z">
</Path>
</Canvas>
</Grid>
</UserControl>


What we are trying to do here is display the thumbnails in a grid, but, as you can see from the above Xaml, I have had to create 3 grids to implement scrolling.
  • LayoutThumbnails - This is the grid which will host the thumbnails
  • LayoutThumbnailsViewport - This is the grid which will host the translated thumbnails
  • LayoutRoot - This is the grid which will host the clipped thumbnails
  • GridTranslate - This exposes the translate transform of LayoutThumbnails
  • GridScrollStoryboard - This is the storyboard that animates GridTranslate to simulate (horizontal) scrolling
Now for some code
public partial class Thumbnails : UserControl
{
Page _parent = null;
int _borderWidth = 2;
int _selectedThumbnailIndex = -1;
int _rowCount = 0;
int _colCount = 0;
int _totalColCount = 0;
int _numPages = 0;
int _currPage = 0;
Size _thumbSize = new Size { Width = 65, Height = 65 };
int _borderPlusPaddingWidth = 0;

public double ControlWidth { get { return (_thumbSize.Width + (_borderPlusPaddingWidth * 2)) * _colCount; }}

public Thumbnails(Page parent)
{
_parent = parent;
_rowCount = _parent.Settings.ThumbnailRows;
_colCount = _parent.Settings.ThumbnailColumns;
_totalColCount = (int)(Math.Ceiling((double)_parent.Settings.ImagesSettings.Count / _rowCount));
_numPages = (int)(Math.Ceiling((double)_totalColCount / _colCount));
_borderPlusPaddingWidth = _parent.Settings.ThumbPadding + _borderWidth;

InitializeComponent();

LayoutRoot.Width = ControlWidth; // ****1****
LayoutThumbnailsViewport.Width = ControlWidth * _numPages; // ****2****

CreateLayout();
DrawThumbnails();
}

int CreateLayout() {
// Thumbnails
LayoutRoot.RowDefinitions.Add(new RowDefinition());
LayoutThumbnailsViewport.SetValue(Grid.RowProperty, 0);

if (_numPages > 0) {
// Navigation Arrows
LayoutRoot.RowDefinitions.Add(new RowDefinition { Height = new GridLength(30) });
RightNav.SetValue(Grid.RowProperty, 1);
LeftNav.SetValue(Grid.RowProperty, 1);
}
}

void DrawThumbnails()
{
for (int row = 0; row < _rowCount; row++) {
LayoutThumbnails.RowDefinitions.Add(new RowDefinition { Height = new GridLength(_thumbSize.Height + (_borderPlusPaddingWidth * 2)) });
}

for (int col = 0; col < _colCount * _numPages; col++) {
LayoutThumbnails.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(_thumbSize.Width + (_borderPlusPaddingWidth * 2)) });
}

int imageIndex = 0;
int startCol = 0;
for (int page = 0; page < _numPages; page++) {
for (int row = 0; row < _rowCount; row++) {
for (int col = 0; col < _colCount && imageIndex < _parent.Settings.ImagesSettings.Count; col++) {
CreateImage(row, col + startCol, imageIndex);
imageIndex++;
}
}
startCol += _colCount;
}

LeftNav.MouseLeftButtonUp += (sender, e) => {
ScrollLeft();
};

RightNav.MouseLeftButtonUp += (sender, e) => {
ScrollRight();
};
}

public void CreateImage(int row, int col, int imageIndex)
{
Image thumb = new Image();
thumb.Source = new BitmapImage(new Uri(_parent.Settings.ThumbPath + _parent.Settings.ImagesSettings[imageIndex].Name, UriKind.Relative));

Border border = new Border {
Width = _thumbSize.Width + (_borderWidth*2),
Height = _thumbSize.Height + (_borderWidth*2),
BorderThickness = new Thickness(_borderWidth),
BorderBrush = new SolidColorBrush(_parent.Settings.FrameColor),
Margin = new Thickness(_parent.Settings.ThumbPadding),
Opacity = 0.6
};
border.SetValue(Grid.RowProperty, row);
border.SetValue(Grid.ColumnProperty, col);
border.SetValue(NameProperty, "Image" + imageIndex.ToString());
border.Tag = imageIndex.ToString(); // Tag can only handle Strings as of SL2 B1 release

border.Child = thumb;
LayoutThumbnails.Children.Add(border);

border.MouseLeftButtonUp += (sender, e) => {
Rect LayoutRootRect = new Rect(0, 0, LayoutRoot.RenderSize.Width, LayoutRoot.RenderSize.Height);
if (LayoutRootRect.Contains(e.GetPosition(LayoutRoot))) { // ****3****
Border currentBorder = (Border)sender;
SelectedThumbnailIndex = Int32.Parse((string)currentBorder.Tag);
}
};
}

bool CanScrollRight { get { return ((_currPage + 1) < _numPages); }}
bool CanScrollLeft { get { return (_currPage > 0); }}

void ScrollRight()
{
if (CanScrollRight) {
_currPage++;
GridScrollAnimation.To = -(_thumbSize.Width + (_borderPlusPaddingWidth * 2)) * _colCount * _currPage;
GridScrollStoryboard.Begin();
}
}

void ScrollLeft()
{
if (CanScrollLeft) {
_currPage--;
GridScrollAnimation.To = -(_thumbSize.Width + (_borderPlusPaddingWidth * 2)) * _colCount * _currPage;
GridScrollStoryboard.Begin();
}
}
}

Most of the code is self explanatory but I would like to elaborate on a few key points which is highlighted in the code:
  1. The width of LayoutThumbnailsViewport should be equal to or greater than the width of the entire grid. If the width is not explicitly set or is less than the width of the entire grid, translatetransform will clip the contents that lie outside the area of the grid
  2. The width of LayoutRoot should be set to the viewing area. This should be less than the width of LayoutThumbnailsViewport if scrolling is desired
  3. One undesired effect of creating this layout is that the underlying grid (LayoutThumbnails) and hence the thumbnails receive mouse events even for the contents that is outside the visible area (LayoutRoot). To circumvent this I had to put this check - if (LayoutRootRect.Contains(e.GetPosition(LayoutRoot))) - in the border.MouseLeftButtonUp event handler. (Note that border is synonymous with thumbnail)

Sunday, May 4, 2008

SimpleViewer-SL Beta 1

Update 5/8/08: Fixed blank screen bug when navigating between thumbnails, and improved browser resizing support...

The Silverlight version of the flash based photo album viewer - SimpleViewer, is here! I decided to call it SimpleViewer-SL for now.

Here are a few examples that were created using SimpleViewer-SL (the keyboard arrow keys are supported as well)
  1. Thumbnails on the left, 3x3 grid

  2. Thumbnails on the right, 5x4 grid

  3. Thumbnails on bottom, 2x4 grid

  4. Thumbnails on the left, with background image

SimpleViewer-SL is highly customizable. It is compatible with SimpleViewer and hence all options exposed by SimpleViewer are applicable to SimpleViewer-SL as well. The options are listed here. The only XML option that is not supported at this time is enableRightClickOpen. Note that the text fields like title and Caption should be plain text (no HTML) for now since I haven't implemented the HTML text control as yet. As for the HTML options, the only supported option is xmlDataPath. The HTML option should be passed to the SimpleViewer-SL Silverlight object using the param tag - <param name="initParams" value="xmlDataPath=gallery1.xml" />

The album can be created using any of the methods described here . After creating the albums, a few modifications to the generated file(s) is required to make it work on SimpleViewer-SL.

The xap can be found here.

The directory structure for the samples is as follows:

gallery1/
SimpleViewer-SL.xap
gallery1.html
gallery1.xml
gallery2.html
gallery2.xml
gallery3.html
gallery3.xml
gallery4.html
gallery4.xml
thumbs/
65x65 thumbnail images
images/
the corresponding big images

Between the above links, the sample html and xml files, it should be fairly straightforward for anyone to figure out how to use SimpleViewer-SL. Overall it is fairly feature complete but there is always room for improvement. It is a little unstable, in that sometime navigating between thumbnails results in a blank screen, but I think it is more to do with Silverlight being a Beta release (looks like it was more to do with my code!).

This exercise took me a little over a week of part time work so I have to say that I am pretty impressed with Silverlight. Most of my time was spent in figuring out a way to work around Silverlight 2 Beta 1 bugs. I suspect that I can simplify the code a bit after the final release of Siverlight 2. Nevertheless, the current size of SimpleViewer-SL is a mere 17kb, the same as SimpleViewer, with lots of opportunity for improvement.

For those looking for the code, I am planning to codeplex it when I get some time. Feel free to leave comments to pressure me into doing it sooner rather than later!

Deep Zoom Composer Updated!

A nice update to Deep Zoom Composer can be found here - http://blogs.msdn.com/expression/archive/2008/05/03/an-update-to-deep-zoom-composer.aspx

The new update includes: improved exporting (includes mousewheel, pan, zoom, and keyboard functionality), better design experience, collection export bug fix, and greater access to help.

Nice!

Friday, April 25, 2008

Next Project - Port SimpleViewer to Silverlight

Having run out of ideas on how further to exploit Deep Zoom, I am going to now focus my energy on porting the closed-source flash based SimpleViewer to Silverlight. Again, my objective is to learn Silverlight, so I will try to push some boundaries (if required). As part of this exercise, I will try to stick to the original goal of SimpleViewer, that is keep the download as small as possible. The current version (1.8.5) of SimpleViewer is only 17k and that is what I am aiming for. This, of course, means no using the extended controls, but only what is available in the core Silverlight 2 SDK!

So, why SimpleViewer? I actually use it in my personal blog and have always wanted more control over it. I also want to know what it takes to create the same experience in Silverlight. I am expecting this to be a trivial exercise, but let's see!

My first roadblock!

I wanted to keep everything as dynamic as possible. This means no explicitly specifying the height and width of any control. I was happy to see that the image control provides this capability and is able to work well within the constraints of MaxWidth and MaxHeight. This was all wonderful and perfect for my needs but now I wanted to draw a simple border around the image. Pretty easy...all I need is the dimensions of the image (control), but this is where it got a little complicated. The image control does give access to the dimensions but a little too late. I won't go into the details here but I did post it on the Silverlight.net forum is anyone's interested. By the way this is a great forum and one of the best places to get answers to all your Silverlight questions or issues.

What I discovered is that the image loaded event is not a reliable place to extract the dimensions from the image control. I tried a kludgey workaround by creating a timer and accessing the dimensions from the image control a little later - around 100 milli seconds - but this is definitely not reliable. But at least I had a solution!

While I was playing with it some more, I discovered the SizeChanged event. This also doesn't help if the image dimensions are extracted from the image control itself, but I discovered that the event itself has the size, and that is when I had my solution!

There might be some issues with this so please leave a comment if you find something. I am assuming this is a bug with the image loaded event so, hopefully, it will be fixed in the next release of Silverlight. But, for now, it looks like I can make some "not so kludgey" progress!

MyImage.SizeChanged += (sender, e) => {
if (e.PreviousSize.Width == 0 && e.PreviousSize.Height == 0 && e.NewSize != e.PreviousSize) {
// e.NewSize contains the dimension of the image
// MyImage.Width and MyImage.ActualWidth is unreliable so use e.NewSize instead
DrawBorderAroundImage(e.NewSize);
}
};

Sunday, April 20, 2008

Deep Zoom - Album Creator

No, this one is not by me!

Marauderz has developed a Deep Zoom Album Creator which uses some code from my blog so I thought I'd mention it here. I am glad to see my code being leveraged in some useful tools, hopefully more of which we shall be seeing soon.

How about some designers getting involved and skinning this baby?

Saturday, April 19, 2008

Deep Zoom - SlideShow

I was wondering how best to showcase the functionality from my last post and the idea of SlideShow popped up in my head. Also, I was bored and wanted to play around with Deep Zoom some more...

Update Oct 14, 2008: Removed the example from this post to reduce load time of this page. Please use the example from this post, DeepZoom sample ported to Silverlight 2 Beta 2, to try the functionality.



Click on the Start Slide button to activate the slide show. Click on the button again to stop the slide show.

The code to enable slide show is very simple. Most of the work is done by the function described in my last post.

Xaml
<Button Grid.Column="2" x:Name="buttonSlideshow" Width="70" Content="Start Slide" IsEnabled="False"/>

Code

int _slideshowImageIndex = 0;
System.Windows.Threading.DispatcherTimer _myDispatcherTimer;

msi.Loaded += delegate(object sender, RoutedEventArgs e)
{
_myDispatcherTimer = new System.Windows.Threading.DispatcherTimer();
_myDispatcherTimer.Tick += new EventHandler(Each_Tick);
buttonSlideshow.IsEnabled = true;
};

buttonSlideshow.Click += delegate(object sender, RoutedEventArgs e)
{
if ((string)buttonSlideshow.Content == "Start Slide") {
buttonSlideshow.Content = "Stop Slide";
StartSlideShow();
}
else {
buttonSlideshow.Content = "Start Slide";
StopSlideShow();
}
};

public void StartSlideShow()
{
// Set the timer interval to 200 ms so that the tick event is called immediately
_myDispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 200);
_myDispatcherTimer.Start();
}

public void StopSlideShow()
{
_myDispatcherTimer.Stop();
}

public void Each_Tick(object o, EventArgs sender)
{
// Reset the timer to 5 sec so that each slide appears after 5 seconds
_myDispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 5000);
ZoomFullAndCenterImage(_slideshowImageIndex);
if (++_slideshowImageIndex == msi.SubImages.Count)
_slideshowImageIndex = 0;
}

Monday, April 14, 2008

Deep Zoom - Zooming and centering image to fill the Deep Zoom screen

One of the readers (see comments here) wanted to zoom and center an image from the collection in such a way that it would fill the screen. Here is the code snippet that will fulfill this requirement. I haven't applied this to an example yet, but I will, if there is enough interest.

public void ZoomFullAndCenterImage(int subImageIndex)
{
Rect subImageRect = GetSubImageRect(subImageIndex);
Rect imageRectElement = msiLogicalToElementRect(GetSubImageRect(subImageIndex));

// Calculate the zoom factor such that the image will fill up the entire screen
double zoomFactor = (msi.Width / imageRectElement.Width) < (msi.Height / imageRectElement.Height) ?
msi.Width / imageRectElement.Width : msi.Height / imageRectElement.Height;

// Center the image
DisplaySubImageCentered(subImageIndex);
// Use the mid point of the image to zoom from
msi.ZoomAboutLogicalPoint(zoomFactor, (subImageRect.Left+subImageRect.Right)/2, (subImageRect.Top+subImageRect.Bottom)/2);
}

Sunday, April 13, 2008

Dissecting Hard Rock Memorabilia...- Part 9 - Source code

Due to the large number of requests for the source code and my inability to respond to every request, I decided to post the link to the source code here.

I would appreciate a quick comment to this post if you do decide to download the source code. This way I get an idea of how much interest there is for the code.

Disclaimer: I used the project created by Scott Hanselman, and parts of the code was borrowed from the Expression Team and Soul Solutions blog. I used Notepad++ as my editor since I do not have access to Visual Studio. As a result of this and the fact that I have very limited time to dedicate to this activity, the code is as dirty as it gets!

Tuesday, April 8, 2008

Dissecting Hard Rock Memorabilia and Silverlight Deep Zoom - Part 8

For completion sake, I decided to post the animation and randomize code here. The code to do the actual animation is lifted as is from the Expression Team blog but I did make a few subtle changes -
  1. Fixed the memory leak issue by getting rid of the Storyboard after it has achieved its purpose
  2. Used a single storyboard to animate multiple images. I don't know if this is better than multiple storyboards (one for each image) so it would be nice to hear some opinions

Code to animate the image

private void AnimateImage(MultiScaleSubImage currentImage, Point futurePosition)
{
// Create Keyframe
SplinePointKeyFrame endKeyframe = new SplinePointKeyFrame();
endKeyframe.Value = futurePosition;
endKeyframe.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1));

KeySpline ks = new KeySpline();
ks.ControlPoint1 = new Point(0, 1);
ks.ControlPoint2 = new Point(1, 1);
endKeyframe.KeySpline = ks;

// Create Animation
PointAnimationUsingKeyFrames moveAnimation = new PointAnimationUsingKeyFrames();
moveAnimation.KeyFrames.Add(endKeyframe);

Storyboard.SetTarget(moveAnimation, currentImage);
Storyboard.SetTargetProperty(moveAnimation, "ViewportOrigin");

// Add the animation to the Storyboard
_moveStoryboard.Children.Add(moveAnimation);
}

Code to invoke the animation - I decided against animating the hidden images (like Hard Rock) since my goal is to work with large amount of images and I didn't want to incur the additional performance overhead

private Storyboard _moveStoryboard;

public void InitStoryboard()
{
_moveStoryboard = new Storyboard();
msi.Resources.Add(_moveStoryboard);

_moveStoryboard.Completed += (sender, e) => msi.Resources.Remove((Storyboard)sender);
}

public void ArrangeImages()
{
InitStoryboard();

...

// Then display the displayable images to scale
for (int i=0; i<_imagesToShow.Count; i++) {
double X = rows[subImages[i].RowNum].CalcX(subImages[i].ColNum);
double Y = margin;
for (int j=0; j<subImages[i].RowNum; j++)
Y += spaceBetweenImages + rows[j].Height;

_imagesToShow[i].ViewportWidth = containerAspectRatio / subImages[i].Width;
AnimateImage(_imagesToShow[i], new Point(-(X / subImages[i].Width), -(Y / subImages[i].Width)));
_imagesToShow[i].Opacity = 1.0;
}

// Play Storyboard
_moveStoryboard.Begin();
}

Code to Randomize the images. The code for the RandomizeListOfImages is lifted from the Expression Team blog

buttonRandomize.Click += (sender, e) => RandomizeAndArrange();

private void RandomizeAndArrange()
{
_imagesToShow = RandomizedListOfImages();
ArrangeImages();
}

private List<MultiScaleSubImage> RandomizedListOfImages()
{
List<MultiScaleSubImage> imageList = new List<MultiScaleSubImage>();
Random ranNum = new Random();

// Store List of Images
_imagesToShow.ForEach(subImage => imageList.Add(subImage));

// Randomize Image List
int numImages = imageList.Count;
for(int i=0; i<numImages; i++) {
MultiScaleSubImage tempImage = imageList[i];
imageList.RemoveAt(i);

int ranNumSelect = ranNum.Next(imageList.Count);

imageList.Insert(ranNumSelect, tempImage);
}
return imageList;
}

Sunday, April 6, 2008

Deep Zoom - Known issues

I spent a few days on the silverlight forums to see if there were any known issues with the current Deep Zoom beta release. The below list of my findings is mainly to help me keep track of the issues so that I don't spend a good amount of my time trying to fix what cannot be fixed (or can it?)!
  1. Source property change is not handled well! There seems to be a workaround for non collection images but in general it seems to be broken - http://silverlight.net/forums/p/11709/44231.aspx#44231
  2. Cross domain not working! Hosting the application and the deep zoom images in different domains does not work - http://silverlight.net/forums/p/13152/43883.aspx#43883
  3. MultiScaleImage loses the image resolution after ScaleTransform! I noticed this when getting in/out of full screen mode as well - http://silverlight.net/forums/p/13014/42316.aspx#42316
  4. MultiScaleImage only supports absolute paths! See my previous post for an example of how Vertigo worked around this (same domain only) - http://silverlight.net/forums/p/12182/40164.aspx#40164
  5. Transparent images are not supported in collection mode? http://forums.expression.microsoft.com/en-US/thread/7f5aace6-9ab1-4503-8b34-f0378b731e98
For those of you who might be thinking of playing around with the bin file format you might want to wait a little while. The final release version of Silverlight 2 Deep Zoom is expected to support the xml file format instead of the bin file format - http://silverlight.net/forums/p/12784/41724.aspx#41724

Thursday, April 3, 2008

Deep Zoom - Highlighting the clicked SubImage

Having played with the Hard Rock demo extensively, I noticed that it is a little difficult to see which image is associated with the displayed metadata. In general, it is the centered image but it could get confusing sometimes. This could be alleviated if there is a highlight or a border around the image.

It's not that hard actually...

1. Changes to the XAML
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="5"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>

<Border x:Name="msiBorder" Grid.Row="0" BorderThickness="1" BorderBrush="Black">
<MultiScaleImage x:Name="msi" UseSprings="false" Margin="2,2,2,2"/>
</Border>
<Canvas Grid.Row="0" Margin="3,3,2,2">
<Border x:Name="imageBorder" BorderThickness="2" BorderBrush="Black" Opacity="0" />
</Canvas>

...

2. The code to show and hide the highlight on a image

public void ShowImageHighlight(int nSubImageIndex)
{ // Hightlight the image by drawing a border around it

int borderWidth = 2; // Setting the default border width to 2
if (msi.ViewportWidth < 1) // This adjusts for zoomed images where a border of 2 is insufficient
borderWidth = (int) (borderWidth / msi.ViewportWidth);

Rect borderRect = ExpandRect(msiLogicalToElementRect(GetSubImageRect(nSubImageIndex)), borderWidth);

imageBorder.Width = borderRect.Width;
imageBorder.Height = borderRect.Height;
imageBorder.SetValue(Canvas.LeftProperty, borderRect.X);
imageBorder.SetValue(Canvas.TopProperty, borderRect.Y);
imageBorder.BorderThickness = new Thickness(borderWidth);
imageBorder.Opacity = 1.0;
}

public void HideImageHighlight()
{
imageBorder.Opacity = 0.0;
}

Rect msiLogicalToElementRect(Rect rect)
{
return new Rect(msi.LogicalToElementPoint(new Point(rect.Left, rect.Top)),
msi.LogicalToElementPoint(new Point(rect.Right, rect.Bottom)));
}

Rect ExpandRect(Rect rect, int expandBy)
{
return new Rect(rect.Left - expandBy, rect.Top - expandBy, rect.Width + expandBy*2, rect.Height + expandBy*2);
}


3. The hooks to invoke the above code

int _clickedImageIndex = -1;

msi.MotionFinished += (sender, e) => { if (_clickedImageIndex >= 0) ShowImageHighlight(_clickedImageIndex); };

msi.MouseLeftButtonDown += (sender, e) => { _clickedImageIndex = -1; };
new MouseWheelHelper(this).Moved += (sender, e) => { _clickedImageIndex = -1; };

msi.MouseLeftButtonUp += delegate(object sender, MouseButtonEventArgs e)
{
if (mouseButtonPressed && !dragInProgress)
{
Point p = this.msi.ElementToLogicalPoint(e.GetPosition(this.msi));
int subImageIndex = SubImageHitTest(p);
if (subImageIndex >= 0) {
DisplaySubImageCentered(subImageIndex);
_clickedImageIndex = subImageIndex;
}

bool shiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (shiftDown) Zoom(0.5, e.GetPosition(this.msi));
else Zoom(2.0, e.GetPosition(this.msi));
}
mouseButtonPressed = false;
dragInProgress = false;
};

I updated the previous example Dissecting Hard Rock Memorabilia and Silverlight Deep Zoom - Part 7 with this code so you can see how it works!

Vertigo BigPicture (aka Hard Rock?) sample code

Vertigo has published their BigPicture code on codeplex. As of this time, only the mouse handler code is available but hopefully this will change. The mouse handler code is clean and well documented though so I am going to use it on my project.

One of the known issues with MultiScaleImages is that it doesn't properly handle relative URI in the Source property. Vertigo has found a neat workaround by converting a relative Uri to an absolute Uri using the Uri.TryCreate method

// Directly lifted from http://www.codeplex.com/BigPicture
Uri collectionUri;

if (Uri.TryCreate(App.Current.Host.Source, "/Collection/items.bin", out collectionUri))
image.Source = collectionUri;

Wednesday, April 2, 2008

Dissecting Hard Rock Memorabilia and Silverlight Deep Zoom - Part 7

One of the readers asked me if I could post code to center the clicked image on the screen. So here goes...

Update Oct 14, 2008: Removed the example from this post to reduce load time of this page. Please use the example from this post, DeepZoom sample ported to Silverlight 2 Beta 2, to try the functionality.



The example above centers and zooms the clicked image.

Here is the code

msi.MouseLeftButtonUp += delegate(object sender, MouseButtonEventArgs e)
{
if (mouseButtonPressed && !dragInProgress)
{
Point p = this.msi.ElementToLogicalPoint(e.GetPosition(this.msi));
int subImageIndex = SubImageHitTest(p);
if (subImageIndex >= 0)
DisplaySubImageCentered(subImageIndex);

bool shiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
if (shiftDown) Zoom(0.5, e.GetPosition(this.msi));
else Zoom(2.0, e.GetPosition(this.msi));
}
mouseButtonPressed = false;
dragInProgress = false;
};


void DisplaySubImageCentered(int indexSubImage)
{
if (indexSubImage < 0 || indexSubImage >= msi.SubImages.Count)
return;

Rect subImageRect = GetSubImageRect(indexSubImage);
double msiAspectRatio = msi.ActualWidth / msi.ActualHeight;

Point newOrigin = new Point(subImageRect.X - (msi.ViewportWidth / 2) + (subImageRect.Width / 2),
subImageRect.Y - ((msi.ViewportWidth / msiAspectRatio) / 2) + (subImageRect.Height / 2));

msi.ViewportOrigin = newOrigin;
}

int SubImageHitTest(Point p)
{
for (int i=0; i<msi.SubImages.Count; i++) {
Rect subImageRect = GetSubImageRect(i);
if (subImageRect.Contains(p))
return i;
}

return -1;
}

Rect GetSubImageRect(int indexSubImage)
{
if (indexSubImage < 0 || indexSubImage >= msi.SubImages.Count)
return Rect.Empty;

MultiScaleSubImage subImage = msi.SubImages[indexSubImage];

double scaleBy = 1 / subImage.ViewportWidth;
return new Rect(-subImage.ViewportOrigin.X * scaleBy, -subImage.ViewportOrigin.Y * scaleBy,
1 * scaleBy, (1 / subImage.AspectRatio) * scaleBy);
}

Tuesday, April 1, 2008

Dissecting Hard Rock Memorabilia and Silverlight Deep Zoom - Part 6

The filter example is finally here!

Update Oct 14, 2008: Removed the example from this post to reduce load time of this page. Please use the example from this post, DeepZoom sample ported to Silverlight 2 Beta 2, to try the functionality.


Click on the Filter button to filter the images based on the tag entered in the text box to the left of the filter button. The following tags will yield some result: lilies, tulip, rose, hibiscus, iris, blue, white, yellow, red, peach, pink, purple. The flower tag will return all the images. The current limitation is that only one tag can be used at a time.

Behind the scenes...

This was actually pretty easy. I used the method suggested by George (see the first comment in this post ) instead of storing a pointer in the Tag property of the MultiScaleSubImage control.

This is what I did to accomplish basic filter functionality:
1. Modified the Deep Zoom generated SparseImageSceneGraph.xml file to include some tag information
?xml version="1.0"?>
<SceneGraph version="1">
<SceneNode>
<FileName>tigerlilies_cr.jpg</FileName>
<ZOrder>1</ZOrder>
<Tags>
<Tag>lilies</Tag>
<Tag>flower</Tag>
</Tags>
</SceneNode>
<SceneNode>
<FileName>tulip_bluebase-1_cr.jpg</FileName>
<ZOrder>5</ZOrder>
<Tags>
<Tag>tulip</Tag>
<Tag>flower</Tag>
<Tag>blue</Tag>
</Tags>
</SceneNode>
<SceneNode>
<FileName>2redandwhitelilies_cr.jpg</FileName>
<ZOrder>8</ZOrder>
<Tags>
<Tag>lilies</Tag>
<Tag>flower</Tag>
<Tag>red</Tag>
<Tag>white</Tag>
</Tags>
</SceneNode>
...
</SceneGraph>

2. Loaded the SparseImageSceneGraph.xml asynchronously into the control. I could have simplified this by loading the metadata file synchronously or by embedding it in the xap package itself but I wanted to learn the asynchronous download feature provided by Silverlight

using System.Net;
using System.Linq;
using System.Xml.Linq;

XElement _xmlImageMetadata; // to store the metadata associated with the images

msi.Loaded += delegate(object sender, RoutedEventArgs e)
{
// Now set the source property.
// This ensures that the html page with the silverlight control is loaded completely before loading the multiscaleimage.
// This is to enhance the overall user experience.
msi.Source = new Uri("http://thepintospatronus.com/deepzoom/test/items.bin");

// Download the metadata associated with the above uri
WebClient _downloader = new WebClient();
_downloader.DownloadStringCompleted += new DownloadStringCompletedEventHandler(imageMetadata_DownloadStringCompleted);
_downloader.DownloadStringAsync(new Uri("http://thepintospatronus.com/deepzoom/test/SparseImageSceneGraph.xml"));
};

void imageMetadata_DownloadStringCompleted(Object sender, DownloadStringCompletedEventArgs e)
{
if (e.Error == null) {

// Convert the string xml representation to a valid XML document
_xmlImageMetadata = XElement.Parse(e.Result);

// The filter button is disabled by default. Enable it when the metadata is available
buttonFilter.IsEnabled = true;
}
}

3. Used Linq to filter the dataset

buttonFilter.Click += delegate(object sender, RoutedEventArgs e)
{
IEnumerable<int> ZOrders =
from sceneNode in _xmlImageMetadata.Elements("SceneNode")
where
(from tag in sceneNode.Elements("Tags").Elements("Tag")
where
((string)tag.Value).ToUpper() == txtFilter.Text.ToUpper()
select tag)
.Any()
select ((int)sceneNode.Element("ZOrder")-1);

_imagesToShow.Clear();
_imagesToHide.Clear();
for (int i=0; i<msi.SubImages.Count; i++) {
if (ZOrders.Contains(i))
_imagesToShow.Add(msi.SubImages[i]);
else
_imagesToHide.Add(msi.SubImages[i]);
}
ArrangeImages();
};

TODO - Better animation.

Comments are welcome!

Monday, March 31, 2008

Dissecting Hard Rock Memorabilia and Silverlight Deep Zoom - Part 5

Trying to find out which image is clicked? Maybe this code snippet will help...

msi.MouseLeftButtonUp += delegate(object sender, MouseButtonEventArgs e)
{
Point p = this.msi.ElementToLogicalPoint(e.GetPosition(this.msi));
int subImageIndex = SubImageHitTest(p);
if (subImageIndex >= 0)
// look up a custom corresponding data structure or xml and return the metadata associated with this image
}

int SubImageHitTest(Point p)
{
for (int i=0; i<msi.SubImages.Count; i++) {
MultiScaleSubImage subImage = msi.SubImages[i];

double scaleBy = 1 / subImage.ViewportWidth;
Rect subImageRect = new Rect(-subImage.ViewportOrigin.X * scaleBy, -subImage.ViewportOrigin.Y * scaleBy,
1 * scaleBy, (1 / subImage.AspectRatio) * scaleBy);
if (subImageRect.Contains(p))
return i;
}

return -1;
}

Dissecting Hard Rock Memorabilia and Silverlight Deep Zoom - Part 4

Update Oct 14, 2008: Removed the example from this post to reduce load time of this page. Please use the example from this post, DeepZoom sample ported to Silverlight 2 Beta 2, to try the functionality.


Click on the Randomize button to randomly arrange the images.

Click on the Full Screen button to get into full screen mode. I am not sure if there is bug in Silverlight or I am doing something wrong but it seems like the MultiScaleImage height and width is not reset after it gets out of the full screen mode! Also, the keyboard seems to be disabled in full screen mode (except for the ESC key)!

Click on the Set Source button to set the source for the MultiScaleImage control to the one specified in the text box to the left of the button. This should be a fully qualified URI and should point to a valid items.bin file (http://.../items.bin). I could not get this to work across domains even after placing a crossdomain.xml file in the root folder of my alternate web host but maybe I was doing something wrong. It seems to work if the file is in the same host but for some reason it is not arranging the images. You can try this url and see what I mean - http://thepintospatronus.com/deepzoom/test2/items.bin

What I learned from this exercise so far...
  • How to use some of the Siverlight 2 controls, position them on the screen so that they scale, and apply borders to them!
  • How to enable full screen mode. The msdn help section refers to some Javascript code to achieve this but I wanted to do it in managed code. Finally figured it out...
    buttonFullScreen.Click += (sender, e) => Application.Current.Host.Content.IsFullScreen = !Application.Current.Host.Content.IsFullScreen;

    Application.Current.Host.Content.FullScreenChanged (sender, e) => // Will be called when the control renders in full screen;
    Application.Current.Host.Content.Resized (sender, e) => // Will be called when the control gets out of full screen;
  • How to host this puppy! Initially I tried silverlight.live.com but then realized that I couldn't upload the deep zoom images up there. So I used my host to upload the images but then realized that the control located at silverlight.live.com couldn't access the images from my host - I even played around with the crossdomain.xml with no success. I then decided to host everything on my host (bluehost.com) since they did support the .xap mime type without any additional tweaking