Showing posts with label Silverlight. Show all posts
Showing posts with label Silverlight. Show all posts

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

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

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!