﻿// Slide.Show, version 1.0
// Copyright © Vertigo Software, Inc.
// This source is subject to the Microsoft Public License (Ms-PL).
// See http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx.
// All other rights reserved.

/// <reference path="Silverlight.js" />

/*******************************************
 * namespace: SlideShow
 *******************************************/
if (!window.SlideShow)
	window.SlideShow = {};

/*******************************************
 * function: SlideShow.createDelegate
 *******************************************/
SlideShow.createDelegate = function(instance, method)
{
	/// <summary>Creates a delegate function that executes the specified method in the correct context.</summary>
	/// <param name="instance">The instance whose method should be executed.</param>
	/// <param name="method">The method to execute.</param>
	/// <returns>The delegate function.</returns>
	
	return function()
	{
		return method.apply(instance, arguments);
	};
};

/*******************************************
 * function: SlideShow.merge
 *******************************************/
SlideShow.merge = function(destination, source, preventNew)
{
	/// <summary>Merges properties from a source object into a destination object.</summary>
	/// <param name="destination">The destination object.</param>
	/// <param name="source">The source object.</param>
	/// <param name="preventNew">Indicates whether or not new properties are prevented from being added to the destination object.</param>
	
	var root = destination;
	
	for (var i in source)
	{
		var s = source[i], d;
		var properties = i.split(".");
		var count = properties.length;
		
		for (var j = 0; j < count; j++)
		{
			i = properties[j];
			d = destination[i];
			
			if (d)
			{
				if (typeof(d) == "object")
					destination = d;
			}
			else if (j > 0 && j < count - 1)
			{
				var obj = {};
				obj[properties.slice(j, count).join(".")] = s;
				s = obj;
				break;
			}
		}
		
		if (d && typeof(d) == "object" && typeof(s) == "object")
			this.merge(d, s, false);
		else if (preventNew && count <= 1 && typeof(d) == "undefined")
			throw new Error("Undefined property: " + i);
		else
			destination[i] = s;
		
		destination = root;
	}
};

/*******************************************
 * function: SlideShow.extend
 *******************************************/
SlideShow.extend = function(baseClass, derivedClass, derivedMembers)
{
	/// <summary>Provides support for class inheritance.</summary>
	/// <param name="baseClass">The base class.</param>
	/// <param name="derivedClass">The derived class.</param>
	/// <param name="derivedMembers">The members to add to the derived class and override in the base class.</param>
	
	var F = function() {};
	F.prototype = baseClass.prototype;
	derivedClass.prototype = new F();
	derivedClass.prototype.constructor = derivedClass;
	derivedClass.base = baseClass.prototype;
	
	if (baseClass.prototype.constructor == Object.prototype.constructor)
		baseClass.prototype.constructor = baseClass;
	
	// Note: IE will not enumerate derived members that exist in the Object
	// prototype (e.g. toString, valueOf). If overriding these members
	// is necessary, search for "_IEEnumFix" for one possible solution.
	if (derivedMembers)
		for (var i in derivedMembers)
			derivedClass.prototype[i] = derivedMembers[i];
};

/*******************************************
 * function: SlideShow.parseBoolean
 *******************************************/
SlideShow.parseBoolean = function(value)
{
	/// <summary>Parses a boolean from the specified value.</summary>
	/// <param name="value">The value to parse.</param>
	/// <returns>True if the specified value is parsed as true.</returns>
	
	return (typeof(value) == "string") ? (value.toLowerCase() == "true") : Boolean(value);
};

/*******************************************
 * function: SlideShow.formatString
 *******************************************/
SlideShow.formatString = function(value)
{
	/// <summary>Formats a string.</summary>
	/// <param name="value">The string to format.</param>
	/// <returns>The formatted string.</returns>
	
	for(i = 1, j = arguments.length; i < j; i++)
		value = value.replace("{" + (i - 1) + "}", arguments[i]);
	
	return value;
};

/*******************************************
 * function: SlideShow.getUniqueId
 *******************************************/
SlideShow.getUniqueId = function(prefix)
{
	/// <summary>Gets a random unique identifier.</summary>
	/// <param name="prefix">The prefix to prepend to the generated identifier.</param>
	/// <returns>The unique identifier.</returns>
	
	return prefix + Math.random().toString().substring(2);
};

/*******************************************
 * function: SlideShow.addTextToBlock
 *******************************************/
SlideShow.addTextToBlock = function(textBlock, text)
{
	/// <summary>Sets the text in the specified block, truncating it to fit if necessary.</summary>
	/// <param name="textBlock">The element to modify.</param>
	/// <param name="text">The text to set.</param>
	
	if (text)
	{
		textBlock.text = text;
		
		var width  = textBlock.width;
		var height = textBlock.height;
		
		try
		{
			if (textBlock.actualWidth <= width && textBlock.actualHeight <= height)
				return;
			
			var min = 0;
			var max = text.length;
			var nonWordChars = /\W*$/;
			var ellipsis = "\u2026";
			
			// binary search
			while (true)
			{
				var mid = Math.floor((max + min) / 2);
				textBlock.text = text.substring(0, mid).replace(nonWordChars, ellipsis);
				
				if (mid == min)
					break;
				
				if (textBlock.actualWidth > width || textBlock.actualHeight > height)
					max = mid;
				else
					min = mid;
			}
			
			// clear if text still doesn't fit
			if (textBlock.actualWidth > width || textBlock.actualHeight > height)
				textBlock.text = null;
		}
		catch (error)
		{
			// ignore if textBlock.actualWidth can't be evaluated
		}
	}
	else
	{
		textBlock.text = null;
	}
};

/*******************************************
 * class: SlideShow.Object
 *******************************************/
SlideShow.Object = function()
{
	/// <summary>Provides a common base class with support for options and events.</summary>
	
	this.options = {};
	this.eventHandlers = {};
};

SlideShow.Object.prototype = 
{
	setOptions: function(options)
	{
		/// <summary>Merges the specified options with existing options.</summary>
		/// <param name="options">The options to merge.</param>
		
		SlideShow.merge(this.options, options, true);
	},
	
	addEventListener: function(name, handler)
	{
		/// <summary>Adds an event handler to the list of handlers to be called when the specified event is fired.</summary>
		/// <param name="name">The event name.</param>
		/// <param name="handler">The event handler to add.</param>
		/// <returns>The identifying token (i.e. index) for the added event handler.</return>
		
		var handlers = this.eventHandlers[name];
		
		if (!handlers)
			this.eventHandlers[name] = handlers = [];
		
		// regarding token, see http://msdn2.microsoft.com/en-us/library/bb232863.aspx
		var token = handlers.length;
		handlers[token] = handler;
		return token;
	},
	
	removeEventListener: function(name, handlerOrToken)
	{
		/// <summary>Removes the first matching event handler from the list of handlers to be called when the specified event is fired.</summary>
		/// <param name="name">The event name.</param>
		/// <param name="handlerOrToken">The event handler or indentifying token to remove.</param>
		
		if (typeof(handlerOrToken) == "function")
		{
			var handlers = this.eventHandlers[name];
			
			if (handlers)
			{
				for (var i = 0, j = handlers.length; i < j; i++)
					if (handlers[i] == handlerOrToken)
						break;
				
				handlers.splice(i, 1);
			}
		}
		else
		{
			handlers.splice(handlerOrToken, 1);
		}
	},
	
	fireEvent: function(name, e)
	{
		/// <summary>Fires the specified event and calls each listening handler.</summary>
		/// <param name="name">The name of the event to fire.</param>
		/// <param name="e">The event arguments to pass to each handler.</param>
		
		var handlers = this.eventHandlers[name];
		
		if (handlers)
			for (var i = 0, j = handlers.length; i < j; i++)
				handlers[i](this, e);			
	},
	
	dispose: function()
	{
		/// <summary>Releases the object from memory.</summary>
		
		this.options = null;
		this.eventHandlers = null;
	}
};

/*******************************************
 * class: SlideShow.JsonParser
 *******************************************/
SlideShow.JsonParser = function(options)
{
	/// <summary>Provides support for JSON parsing.</summary>
	/// <param name="options">The options for the parser.</param>

	SlideShow.JsonParser.base.constructor.call(this);
	
	SlideShow.merge(this.options,
	{
		arrays: null
	});
	
	this.setOptions(options);
	
	this.initializeForcedArrays();
};

SlideShow.extend(SlideShow.Object, SlideShow.JsonParser,
{
	initializeForcedArrays: function()
	{
		/// <summary>Converts the "arrays" option into a hash for lookups.</summary>
		
		this.forcedArrays = {};
		
		if (this.options.arrays)
		{
			var items = this.options.arrays.split(",");
			
			for (var i = 0, j = items.length; i < j; i++)
				this.forcedArrays[items[i]] = true;
		}
	},
	
	fromFeed: function(url, callback)
	{
		/// <summary>Adds an external script tag that references the specified JSON feed which calls the specified callback function.</summary>
		/// <param name="url">The location of the feed.</param>
		/// <param name="callback">The name of the callback function.</param>
		
		window[callback] = SlideShow.createDelegate(this, this.onFeedCallback);
		var scriptId = SlideShow.getUniqueId("SlideShow_Script_");
		SlideShow.ScriptManager.addExternalScript(scriptId, "text/javascript", url);
	},
	
	onFeedCallback: function(obj)
	{
		/// <summary>Handles the callback from a JSON feed and fires the "callback" event.</summary>
		/// <param name="obj">The returned JSON object.</param>
		
		this.fireEvent("callback", obj);
	},	
	
	fromXml: function(url, async)
	{
		/// <summary>Parses a JSON object from the specified XML file and fires the "parseComplete" event.</summary>
		/// <param name="url">The location of the file to parse.</param>
		/// <param name="async">Specifies whether or not to parse the XML asynchronously.</param>
		
		var request;
		
		if (window.XMLHttpRequest)
			request = new window.XMLHttpRequest();
		else if (window.ActiveXObject)
			request = new window.ActiveXObject("Microsoft.XMLHTTP");
		else
			throw new Error("XML parsing failed: Unsupported browser");
		
		var handleReadyStateChange = function()
		{
			if (request.readyState == 4) 
			{
				if (request.status == 200)
				{
					var document = request.responseXML;
					var obj = this.parseXmlDocument(document);
					this.fireEvent("parseComplete", obj);
				}
				else
				{
					throw new Error("XML parsing failed: " + request.statusText);
				}
			}
		};
		
		if (async)
		{
			request.onreadystatechange = SlideShow.createDelegate(this, handleReadyStateChange);
			request.open("GET", url, true);
			request.send(null);
		}
		else
		{
			request.open("GET", url, false);
			request.send(null);
			handleReadyStateChange.apply(this);
		}
	},
	
	parseXmlDocument: function(document)
	{
		/// <summary>Parses a JSON object from the specified XML document.</summary>
		/// <param name="document">The document to parse.</param>
		/// <returns>The parsed object.</returns>
		
		var element = document.documentElement;
		
		if (!element)
			return;
		
		var elementName = element.nodeName;
		var elementType = element.nodeType;
		var elementValue = this.parseXmlNode(element);
		
		if (this.forcedArrays[elementName])
			elementValue = [ elementValue ];
		
		// document fragment
		if (elementType == 11)
			return elementValue;
		
		var obj = {};
		obj[elementName] = elementValue;
		return obj;
	},
	
	parseXmlNode: function(node)
	{
		/// <summary>Recursively parses a JSON object from the specified XML node.</summary>
		/// <param name="element">The node to parse.</param>
		/// <returns>The parsed object.</returns>
		
		switch (node.nodeType)
		{
			// comment
			case 8:
				return;
			
			// text and cdata
			case 3:
			case 4:
			
				var nodeValue = node.nodeValue;
				
				if (!nodeValue.match(/\S/))
					return;
				
				return this.formatValue(nodeValue);
			
			default:
				
				var obj;
				var counter = {};
				var attributes = node.attributes;
				var childNodes = node.childNodes;
				
				if (attributes && attributes.length)
				{
					obj = {};
					
					for (var i = 0, j = attributes.length; i < j; i++)
					{
						var attribute = attributes[i];
						var attributeName = attribute.nodeName.toLowerCase(); // lowered in order to be consistent with Safari
						
						if (typeof(attributeName) != "string")
							continue;
						
						var attributeValue = attribute.nodeValue;
						
						if (!attributeValue)
							continue;
						
						if (typeof(counter[attributeName]) == "undefined")
							counter[attributeName] = 0;
						
						this.addProperty(obj, attributeName, this.formatValue(attributeValue), ++counter[attributeName]);
					}
				}
				
				if (childNodes && childNodes.length)
				{
					var textOnly = true;
					
					if (obj)
						textOnly = false;
					
					for (var k = 0, l = childNodes.length; k < l && textOnly; k++)
					{
						var childNodeType = childNodes[k].nodeType;
						
						// text or cdata
						if (childNodeType == 3 || childNodeType == 4)
							continue;
						
						textOnly = false;
					}
					
					if (textOnly)
					{
						if (!obj)
							obj = "";
					
						for (var m = 0, n = childNodes.length; m < n; m++)
							obj += this.formatValue(childNodes[m].nodeValue);
					}
					else
					{
						if (!obj)
							obj = {};
						
						for (var o = 0, p = childNodes.length; o < p; o++)
						{
							var childNode = childNodes[o];
							var childName = childNode.nodeName;
							
							if (typeof(childName) != "string")
								continue;
							
							var childValue = this.parseXmlNode(childNode);
							
							if (!childValue)
								continue;
							
							if (typeof(counter[childName]) == "undefined")
								counter[childName] = 0;
							
							this.addProperty(obj, childName, this.formatValue(childValue), ++counter[childName]);
						}
					}
				}
				
				return obj;
		}
	},
	
	formatValue: function(value)
	{
		/// <summary>Formats the specified value to its most suitable type.</summary>
		/// <param name="value">The value to format.</param>
		/// <returns>The formatted value or the original value if no more suitable type exists.</returns>
		
		if (typeof(value) == "string")
		{
			var loweredValue = value.toLowerCase();
			
			if (loweredValue == "true")
				return true;
			else if (loweredValue == "false")
				return false;
			
			if (!isNaN(value))
				return new Number(value).valueOf(); // fixes number issue with option values
		}
		
		return value;
	},
	
	addProperty: function(obj, name, value, count)
	{
		/// <summary>Adds a property to the specified object.</summary>
		/// <param name="obj">The target object.</param>
		/// <param name="name">The name of the property.</param>
		/// <param name="value">The value of the property.</param>
		/// <param name="count">A count that indicates whether or not the property should be an array.</param>
		
		if (this.forcedArrays[name])
		{
			if (count == 1)
				obj[name] = [];
			
			obj[name][obj[name].length] = value;
		}
		else
		{
			switch (count)
			{
				case 1:
					obj[name] = value;
					break;
				
				case 2:
					obj[name] = [ obj[name], value ];
					break;
				
				default:
					obj[name][obj[name].length] = value;
					break;
			}
		}
	}	
});

/*******************************************
 * class: SlideShow.XmlConfigProvider
 *******************************************/
SlideShow.XmlConfigProvider = function(options)
{
	/// <summary>Provides configuration data to the Slide.Show control from an XML file.</summary>

	SlideShow.XmlConfigProvider.base.constructor.call(this);
	
	SlideShow.merge(this.options,
	{
		url: "Configuration.xml"
	});
	
	this.setOptions(options);
};

SlideShow.extend(SlideShow.Object, SlideShow.XmlConfigProvider,
{
	getConfig: function(configHandler)
	{
		/// <summary>Retrieves the configuration data synchronously and calls the specified event handler (with the data).</summary>
		/// <param name="configHandler">The event handler to be called after the configuration data is retrieved.</param>
		
		var parser = new SlideShow.JsonParser({ arrays: "module,option,script,transition" });
		parser.addEventListener("parseComplete", configHandler);
		parser.fromXml(this.options.url, false);
	}
});

/*******************************************
 * class: SlideShow.ScriptManager
 *******************************************/
SlideShow.ScriptManager = function()
{
	/// <summary>Provides support for loading scripts dynamically.</summary>
	
	SlideShow.ScriptManager.base.constructor.call(this);
	this.scripts = {};
	this.timeoutId = null;
};

SlideShow.extend(SlideShow.Object, SlideShow.ScriptManager,
{
	register: function(key, url)
	{
		/// <summary>Registers a script to be loaded.</summary>
		/// <param name="key">The unique key that identifies the script.</param>
		/// <param name="url">The location of the script.</param>
		
		if (this.scripts[key])
			throw new Error("Duplicate script: " + key);
		
		this.scripts[key] = { url: url, loaded: false };
	},
	
	load: function()
	{
		/// <summary>Loads the registered scripts.</summary>
		
		for (var key in this.scripts)
		{
			var id = "SlideShow_Script_" + key;
			SlideShow.ScriptManager.addExternalScript(id, "text/javascript", this.scripts[key].url);
		}
		
		// timeout after 15 seconds
		this.timeoutId = window.setTimeout(SlideShow.createDelegate(this, this.onLoadTimeout), 15000);
		this.checkLoadStatus();
	},
	
	checkLoadStatus: function()
	{
		/// <summary>Fires the "loadComplete" event when all registered scripts are loaded.</summary>
		
		for (var key in this.scripts)
		{
			var script = this.scripts[key];
			
			if (!script.loaded)
			{
				if (typeof(eval("SlideShow." + key)) == "undefined")
				{
					window.setTimeout(SlideShow.createDelegate(this, this.checkLoadStatus), 100);
					return;
				}
				else
				{
					script.loaded = true;
				}
			}
		}
		
		if (this.timeoutId)
		{
			window.clearTimeout(this.timeoutId);
			this.timeoutId = null;
		}
		
		this.fireEvent("loadComplete");
	},
	
	onLoadTimeout: function()
	{
		/// <summary>Handles the event fired when the registered scripts fail to load within the allowed time.</summary>
		
		this.timeoutId = null;
		throw new Error("Scripts failed to load in time");
	}
});

SlideShow.ScriptManager.addExternalScript = function(id, type, url)
{
	/// <summary>Adds an external script tag to the document head.</summary>
	/// <param name="id">The ID that uniquely identifies the script element.</param>
	/// <param name="type">The script type.</param>
	/// <param name="url">The script location.</param>
	
	if (!document.getElementById(id))
	{
		var element = document.createElement("script");
		element.id = id;
		element.type = "text/javascript";
		element.src = url;
		document.getElementsByTagName("head")[0].appendChild(element);
	}
};

SlideShow.ScriptManager.addInlineScript = function(id, type, text)
{
	/// <summary>Adds an inline script tag to the document head.</summary>
	/// <param name="id">The ID that uniquely identifies the script element.</param>
	/// <param name="type">The script type.</param>
	/// <param name="text">The script text.</param>
	
	if (!document.getElementById(id))
	{
		var element = document.createElement("script");
		element.id = id;
		element.type = type;
		element.text = text;
		
		try
		{
			// Safari workaround
			element.innerText = text;
		}
		catch (error)
		{
		}
		
		document.getElementsByTagName("head")[0].appendChild(element);
	}
};

/*******************************************
 * class: SlideShow.UserControl
 *******************************************/
SlideShow.UserControl = function(control, parent, xaml, options)
{
	/// <summary>Provides a base class for user controls.</summary>
	/// <param name="control">The Slide.Show control.</param>
	/// <param name="parent">The parent control.</param>
	/// <param name="xaml">The XAML for the control.</param>
	/// <param name="options">The options for the control.</param>
	
	SlideShow.UserControl.base.constructor.call(this);
	
	SlideShow.merge(this.options,
	{
		top: "Auto",
		left: "Auto",
		bottom: "Auto",
		right: "Auto",
		width: "Auto",
		height: "Auto",
		background: "Transparent",
		opacity: 1,
		visibility: "Visible",
		zIndex: 0,
		cursor: "Default"
	});
	
	this.setOptions(options);
	
	this.control = control;
	this.children = [];
	
	if (parent)
	{
		this.parent = parent;
		this.parent.children.push(this);
	}
	
	if (xaml)
		this.root = control.host.content.createFromXaml(xaml, true);
	
	if (this.parent && this.parent.root && this.root)
		this.parent.root.children.add(this.root);
};

SlideShow.extend(SlideShow.Object, SlideShow.UserControl,
{
	render: function()
	{
		/// <summary>Renders the control using the current options.</summary>
		
		this.resize(this.options.width, this.options.height);
		this.reposition();
		
		this.root.background = this.options.background;
		this.root.opacity = this.options.opacity;
		this.root.visibility = this.options.visibility;
		this.root["Canvas.ZIndex"] = this.options.zIndex;
		this.root.cursor = this.options.cursor;
		
		for (var i = 0, j = this.children.length; i < j; i++)
			this.children[i].render();
	},
	
	resize: function(width, height)
	{
		/// <summary>Resizes the control.</summary>
		/// <param name="width">The width.</param>
		/// <param name="height">The height.</param>
		
		var auto = "Auto";
		var currentWidth = this.root.width;
		var currentHeight = this.root.height;
		
		this.root.width = (width != auto) ? Math.max(width, 0) : 0;
		this.root.height = (height != auto) ? Math.max(height, 0) : 0;
		
		if (currentWidth != this.root.width || currentHeight != this.root.height)
			this.onSizeChanged();
	},
	
	reposition: function()
	{
		/// <summary>Positions the control relative to the parent control.</summary>
		
		var auto = "Auto";
		var currentWidth = this.root.width;
		var currentHeight = this.root.height;
		
		this.root["Canvas.Top"] = (this.options.top != auto) ? this.getPosition("top", this.options.top) : 0;
		this.root["Canvas.Left"] = (this.options.left != auto) ? this.getPosition("left", this.options.left) : 0;
		
		if (this.options.bottom != auto)
		{
			if (this.options.height != auto && this.options.top == auto)
				this.root["Canvas.Top"] = this.parent.root.height - this.root.height - this.getPosition("bottom", this.options.bottom);
			else if (this.options.height == auto && this.options.top != auto)
				this.root.height = Math.max(this.parent.root.height - this.root["Canvas.Top"] - this.getPosition("bottom", this.options.bottom), 0);
		}
		
		if (this.options.right != auto)
		{
			if (this.options.width != auto && this.options.left == auto)
				this.root["Canvas.Left"] = this.parent.root.width - this.root.width - this.getPosition("right", this.options.right);
			else if (this.options.width == auto && this.options.left != auto)
				this.root.width = Math.max(this.parent.root.width - this.root["Canvas.Left"] - this.getPosition("right", this.options.right), 0);
		}
		
		if (currentWidth != this.root.width || currentHeight != this.root.height)
			this.onSizeChanged();
	},
	
	getPosition: function(name, value)
	{
		/// <summary>Gets the normalized position.</summary>
		/// <param name="name">The position direction (e.g. top, left, bottom, right).</param>
		/// <param name="value">The position value (e.g. -106, 50%).</param>
		/// <returns>The normalized position (in pixels).</returns>
		
		if (!isNaN(value))
			return value;
		
		// trim trailing "%"
		var percent = value.slice(0, value.length - 1) / 100;
		
		switch (name)
		{
			case "top": return this.parent.root.height * percent - this.root.height / 2;
			case "left": return this.parent.root.width * percent - this.root.width / 2;
			case "bottom": return (1 - this.parent.root.height * percent) - this.root.height / 2;
			case "right": return (1 - this.parent.root.width * percent) - this.root.width / 2;
			default: throw new Error("Invalid name: " + name);
		}
	},
	
	dispose: function()
	{
		/// <summary>Releases the control from memory.</summary>
		
		SlideShow.UserControl.base.dispose.call(this);
		
		if (this.parent)
		{
			for (var i = 0, j = this.parent.children.length; i < j; i++)
				if (this.parent.children[i] == this)
					break;
			
			this.parent.children.splice(i, 1);
			this.parent.root.children.remove(this.root);
		}
		
		this.control = null;
		this.parent = null;
		this.children = null;
		this.root = null;
	},
	
	onSizeChanged: function()
	{
		/// <summary>Handles the event fired when the control is resized.</summary>
		
		for (var i = 0, j = this.children.length; i < j; i++)
			this.children[i].reposition();
	}
});

/*******************************************
 * class: SlideShow.Control
 *******************************************/
SlideShow.Control = function(options)
{
	/// <summary>Initializes and renders a Slide.Show control.</summary>
	/// <param name="options">The options for the control.</param>
	
	SlideShow.Control.base.constructor.call(this);
	
	SlideShow.merge(this.options,
	{
		id: null,
		width: 640,
		height: 480,
		background: "Black",
		windowless: false,
		frameRate: 48,
		enableFrameRateCounter: false,
		enableRedrawRegions: false,
		enableTrace: true,
		installInPlace: true,
		installUnsupportedBrowsers: false,
		cssClass: "SlideShow",
		scripts: null,
		modules: null,
		transitions: null,
		dataProvider: null
	});
	
	if (options instanceof SlideShow.XmlConfigProvider)
	{
		var configProvider = options;
		configProvider.getConfig(SlideShow.createDelegate(this, this.onConfigLoad));
	}
	else
	{
		this.onConfigLoad(this, { configuration: options });
	}
};

SlideShow.extend(SlideShow.UserControl, SlideShow.Control,
{
	render: function()
	{
		/// <summary>Renders the control using the current options.</summary>
		
		SlideShow.Control.base.render.call(this);
		
		this.host.settings.enableFramerateCounter = this.options.enableFrameRateCounter;
		this.host.settings.enableRedrawRegions = this.options.enableRedrawRegions;
		
		if (this.options.enableTrace)
		{
			this.traceLog = this.host.content.createFromXaml('<TextBlock Canvas.Top="10" Canvas.Left="10" Canvas.ZIndex="999" Foreground="#66FFFFFF" FontSize="10" />');
			this.root.children.add(this.traceLog);
		}		
	},
	
	createObject: function()
	{
		/// <summary>Creates and adds the necessary DOM elements for the control.</summary>
		
		this.id = this.options.id || SlideShow.getUniqueId("SlideShow_");
		var sourceElementId = "SlideShow_Source";
		var parentElementId = this.id;
		var objectElementId = parentElementId + "_Object";
		
		// Add the source element
		// Note: Inline XAML used to be buggy in Firefox, but has been fixed in the latest update of Silverlight 1.0
		var xaml = '<Canvas xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="Control" Visibility="Collapsed"></Canvas>';
		SlideShow.ScriptManager.addInlineScript(sourceElementId, "text/xaml", xaml);
		
		// Add the parent element
		// Note: We're using document.write instead of document.appendChild because the latter will not always work correctly (e.g. in tables).
		document.write('<div id="' + parentElementId + '" class="' + this.options.cssClass + '"></div>');
		
		// Add the object element
		// Note: See http://msdn2.microsoft.com/en-us/library/bb412401.aspx for createObjectEx documentation.
		Silverlight.createObjectEx(
		{
			id: objectElementId,
			source: "#" + sourceElementId,
			parentElement: document.getElementById(parentElementId),
			properties:
			{
				width: String(this.options.width),
				height: String(this.options.height),
				background: this.options.background,
				isWindowless: String(this.options.windowless),
				framerate: String(this.options.frameRate),
				inplaceInstallPrompt: SlideShow.parseBoolean(this.options.installInPlace),
				ignoreBrowserVer: SlideShow.parseBoolean(this.options.installUnsupportedBrowsers),
				version: "1.0"
			},
			events:
			{
				onLoad: SlideShow.createDelegate(this, this.onObjectLoad)
			}
		});
	},
	
	getTypeFromConfig: function(config)
	{
		/// <summary>Gets the type from the specified configuration.</summary>
		/// <param name="config">The configuration.</param>
		/// <returns>The evaluated type.</returns>
		
		var type = eval("SlideShow." + config.type);
		
		if (!type)
			throw new Error("Invalid type: " + config.type);
		
		return type;
	},
	
	getOptionsFromConfig: function(config)
	{
		/// <summary>Gets options (in hash format for lookups) from the specified configuration.</summary>
		/// <param name="config">The configuration.</param>
		/// <returns>The options hash.</returns>
		
		var options = {};
		
		if (config.option)
		{
			for (var i = 0, j = config.option.length; i < j; i++)
			{
				var name = config.option[i]["name"];
				var value = config.option[i]["value"];
				options[name] = value;
			}
		}
		
		return options;
	},
	
	createObjectInstanceFromConfig: function(config)
	{
		/// <summary>Creates an instance of the configured object.</summary>
		/// <param name="config">The configuration.</param>
		/// <returns>The instance.</returns>
		
		var type = this.getTypeFromConfig(config);
		var options = this.getOptionsFromConfig(config);
		return new type(this, options);
	},
	
	createModuleInstanceFromConfig: function(config)
	{
		/// <summary>Creates an instance of the configured module.</summary>
		/// <param name="config">The configuration.</param>
		/// <returns>The instance.</returns>
		
		var type = this.getTypeFromConfig(config);
		var options = this.getOptionsFromConfig(config);
		return new type(this, this, options);
	},
	
	loadScripts: function()
	{
		/// <summary>Loads the scripts configured for the control.</summary>
		
		if (this.options.scripts && this.options.scripts.script)
		{
			var manager = new SlideShow.ScriptManager();
			manager.addEventListener("loadComplete", SlideShow.createDelegate(this, this.onScriptsLoad));
			
			for (var i = 0, j = this.options.scripts.script.length; i < j; i++)
			{
				var script = this.options.scripts.script[i];
				manager.register(script.key, script.url);
			}
			
			manager.load();
		}
		else
		{
			this.onScriptsLoad(this);
		}
	},
	
	loadModules: function()
	{
		/// <summary>Loads the modules configured for the control.</summary>
		
		if (this.options.modules && this.options.modules.module)
		{
			var modules = {};
			
			for (var i = 0, j = this.options.modules.module.length; i < j; i++)
			{
				var config = this.options.modules.module[i];
				var module = modules[config.type] = this.createModuleInstanceFromConfig(config);
				module.render();
			}
			
			this.onModulesLoad(this, modules);
		}
	},
	
	loadData: function()
	{
		/// <summary>Loads the data configured for the control.</summary>
		
		if (this.options.dataProvider)
		{
			var provider = this.createObjectInstanceFromConfig(this.options.dataProvider);
			provider.getData(SlideShow.createDelegate(this, this.onDataLoad));
		}
	},
	
	isAlbumIndexValid: function(albumIndex)
	{
		/// <summary>Determines whether or not the specified album index is valid.</summary>
		/// <param name="albumIndex">The album index to check.</param>
		/// <returns>True if the album index is valid.</returns>
		
		return this.data && this.data.album && this.data.album[albumIndex];
	},
	
	isSlideIndexValid: function(albumIndex, slideIndex)
	{
		/// <summary>Determines whether or not the specified album and slide indexes are valid.</summary>
		/// <param name="albumIndex">The album index to check.</param>
		/// <param name="slideIndex">The slide index to check.</param>
		/// <returns>True if the indexes are valid.</returns>
		
		if (this.isAlbumIndexValid(albumIndex))
			return this.data.album[albumIndex].slide && this.data.album[albumIndex].slide[slideIndex];
		
		return false;
	},
	
	getSlideTransitionData: function(albumIndex, slideIndex)
	{
		/// <summary>Gets the transition data for a slide.</summary>
		/// <param name="albumIndex">The album index.</param>
		/// <param name="slideIndex">The slide index.</param>
		/// <returns>The transition data.</returns>
		
		var transitionName;
		
		if (!this.transitions)
			this.transitions = { notransition: { type: "NoTransition" } };
		
		if (this.isSlideIndexValid(albumIndex, slideIndex))
			transitionName = this.data.album[albumIndex].slide[slideIndex].transition;
		
		if (!transitionName && this.isAlbumIndexValid(albumIndex))
			transitionName = this.data.album[albumIndex].transition;
		
		if (!transitionName && this.data)
			transitionName = this.data.transition;
		
		if (!transitionName)
			transitionName = "NoTransition";
		
		var key = transitionName.toLowerCase();
		var transition = this.transitions[key];
		
		if (!transition)
		{
			for (var i = 0, j = this.options.transitions.transition.length; i < j; i++)
			{
				if (this.options.transitions.transition[i].name.toLowerCase() == key)
				{
					transition = this.options.transitions.transition[i];
					break;
				}
			}
			
			if (transition)
				this.transitions[key] = transition;
			else
				throw new Error("Invalid transition: " + transitionName);
		}
		
		return transition;
	},
	
	resize: function(width, height)
	{
		/// <summary>Resizes the control.</summary>
		/// <param name="width">The width value.</param>
		/// <param name="height">The height value.</param>
		
		this.host.setAttribute("width", width);
		this.host.setAttribute("height", height);
	},
	
	showEmbeddedMode: function()
	{
		/// <summary>Shows the control in embedded mode.</summary>
		
		this.host.content.fullScreen = false;
	},
	
	showFullScreenMode: function()
	{
		/// <summary>Shows the control in full-screen mode.</summary>
		
		this.host.content.fullScreen = true;
	},
	
	toggleFullScreenMode: function()
	{
		/// <summary>Toggles the control between embedded and full-screen mode.</summary>
		
		this.host.content.fullScreen = !this.host.content.fullScreen;
	},
	
	isFullScreenMode: function()
	{
		/// <summary>Specifies whether or not the control is in full-screen mode.</summary>
		/// <returns>True if the control is in full-screen mode.</returns>
		
		return this.host.content.fullScreen;
	},	
	
	trace: function(message)
	{
		/// <summary>Prepends a message to the on-screen trace log.</summary>
		/// <param name="message">The message to display.</param>
		
		if (this.traceLog)
		{
			if (this.traceLog.actualHeight > this.host.content.actualHeight - 10)
				this.traceLog.text = "";
			
			this.traceLog.text = message + "\n" + this.traceLog.text;
		}
	},
	
	onObjectLoad: function(host, context, root)
	{
		/// <summary>Handles the event fired when the Silverlight object is loaded.</summary>
		/// <param name="host">The host object.</param>
		/// <param name="context">The user context.</param>
		/// <param name="root">The root element.</param>
		
		this.root = root;
		this.host = host;
		
		this.render();
		this.onResize(this); // fixes a resize issue when width and height is set directly by createObjectEx
		
		this.host.content.onResize = SlideShow.createDelegate(this, this.onResize);
		this.host.content.onFullScreenChange = SlideShow.createDelegate(this, this.onFullScreenChange);
		//this.host.focus(); // enables initial focus in IE, but behaves oddly in Firefox
		
		this.loadScripts();
		this.fireEvent("objectLoad");
	},
	
	onConfigLoad: function(sender, e)
	{
		/// <summary>Handles the event fired when the configuration is loaded.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.fireEvent("configLoad");
		this.setOptions(e.configuration);
		this.createObject();
	},
	
	onScriptsLoad: function(sender, e)
	{
		/// <summary>Handles the event fired when the configured scripts are loaded.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.fireEvent("scriptsLoad");
		this.loadModules();
		this.loadData();
	},
	
	onModulesLoad: function(sender, e)
	{
		/// <summary>Handles the event fired when the configured modules are loaded.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.modules = e;
		this.fireEvent("modulesLoad");
	},
	
	onDataLoad: function(sender, e)
	{
		/// <summary>Handles the event fired when the configured data is loaded.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.data = e.data;
		
		if (this.data)
		{
			if (this.data.startalbumindex && !this.isAlbumIndexValid(this.data.startalbumindex))
				throw new Error("Invalid configuration: startAlbumIndex");
			
			if (this.data.startslideindex && !this.isSlideIndexValid((this.data.startalbumindex)? this.data.startalbumindex : 0, this.data.startslideindex))
				throw new Error("Invalid configuration: startSlideIndex");
		}
		
		this.fireEvent("dataLoad");
	},
	
	onResize: function(sender, e)
	{
		/// <summary>Handles the event fired when the control is resized in embedded mode.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		SlideShow.Control.base.resize.call(this, this.host.content.actualWidth, this.host.content.actualHeight);
		//this.fireEvent("resize");
	},
	
	onFullScreenChange: function(sender, e)
	{
		/// <summary>Handles the event fired when the control changes between embedded and full-screen mode.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.onResize(this);
		this.fireEvent("fullScreenChange");
	}
});

//var initCounter = 0;
//var loadCounter = 0;

/*******************************************
 * class: SlideShow.PageContainer
 *******************************************/
SlideShow.PageContainer = function(control, parent, options)
{
	/// <summary>A resizable control that renders pages of controls.</summary>
	/// <param name="control">The Slide.Show control.</param>
	/// <param name="parent">The parent control.</param>
	/// <param name="options">The options for the control.</param>
	
	var xaml =
		'<Canvas xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="PageContainer" Visibility="Collapsed">' +
		'	<Canvas.Resources>' +
		'		<Storyboard x:Name="storyboard">' +
		'			<DoubleAnimationUsingKeyFrames Storyboard.TargetName="currentPageTransform" Storyboard.TargetProperty="X"> ' +
		'				<SplineDoubleKeyFrame x:Name="currentPageSplineFrom" KeySpline="0,0 0,0" KeyTime="0:0:0" />' +
		'				<SplineDoubleKeyFrame x:Name="currentPageSplineTo" KeySpline="0,0 0,1" />' +
		'			</DoubleAnimationUsingKeyFrames> ' +
		'			<DoubleAnimationUsingKeyFrames Storyboard.TargetName="nextPageTransform" Storyboard.TargetProperty="X"> ' +
		'				<SplineDoubleKeyFrame x:Name="nextPageSplineFrom" KeySpline="0,0 0,0" KeyTime="0:0:0" />' +
		'				<SplineDoubleKeyFrame x:Name="nextPageSplineTo" KeySpline="0,0 0,1" />' +
		'			</DoubleAnimationUsingKeyFrames> ' +
		'		</Storyboard>' +
		'	</Canvas.Resources>' +
		'	<Canvas.Clip>' +
		'		<RectangleGeometry x:Name="centerClip" />' +
		'	</Canvas.Clip>' +
		'	<Canvas x:Name="currentPage">' +
		'		<Canvas.RenderTransform>' +
		'			<TranslateTransform x:Name="currentPageTransform" />' +
		'		</Canvas.RenderTransform>' +
		'	</Canvas>' +
		'	<Canvas x:Name="nextPage">' +
		'		<Canvas.RenderTransform>' +
		'			<TranslateTransform x:Name="nextPageTransform" />' +
		'		</Canvas.RenderTransform>' +
		'	</Canvas>' +
		'</Canvas>';
	
	SlideShow.PageContainer.base.constructor.call(this, control, parent, xaml);

	SlideShow.merge(this.options,
	{
		top: 0,
		left: 0,
		right: 0,
		bottom: 0,
		itemWidth: 220,
		itemHeight: 80,
		padding: 10,
		spacing: 10,
		animatePageChanges: true,
		animationDuration: 0.6
	});
	
	this.setOptions(options);
	
	this.columns = 0;
	this.rows = 0;
	this.itemCountPerPage = 0;	
	this.pageIndex = 0;
	this.pageCount = 0;
	this.currentPage = this.root.findName("currentPage");
	this.nextPage = this.root.findName("nextPage");
	this.centerClip = this.root.findName("centerClip");
	this.currentPageTransform = this.root.findName("currentPageTransform");
	this.nextPageTransform = this.root.findName("nextPageTransform");
	this.storyboard = this.root.findName("storyboard");
	this.currentPageSplineFrom = this.root.findName("currentPageSplineFrom");
	this.currentPageSplineTo = this.root.findName("currentPageSplineTo");
	this.nextPageSplineFrom = this.root.findName("nextPageSplineFrom");
	this.nextPageSplineTo = this.root.findName("nextPageSplineTo");
	
	this.storyboard.addEventListener("Completed", SlideShow.createDelegate(this, this.onStoryboardComplete));
};

SlideShow.extend(SlideShow.UserControl, SlideShow.PageContainer,
{
	render: function()
	{
		/// <summary>Renders the control using the current options.</summary>
		
		SlideShow.PageContainer.base.render.call(this);
		
		this.currentPage.visibility = "Visible";
		this.nextPage.visibility = "Collapsed";		
	},
	
	determineItemFit: function(itemSize, containerSize)
	{
		/// <summary>Determines how many items can fit within a container.</summary>
		/// <param name="itemSize">The item size.</param>
		/// <param name="containerSize">The container size.</param>
		/// <returns>The number of items.</returns>
		
		if (containerSize == 0 || itemSize == 0)
			return 0;
		
		var spacePerSpacer = this.options.spacing / containerSize;
		var spacePerItem = itemSize / containerSize;
		var totalSpacePerItem = spacePerSpacer + spacePerItem;
		var containerSpace = 1 - spacePerSpacer;
		return Math.floor(containerSpace / totalSpacePerItem);
	},
	
	determineCanvasPosition: function(itemSize, itemIndex)
	{
		/// <summary>Determines where to position an item.</summary>
		/// <param name="itemSize">The item size.</param>
		/// <param name="itemIndex">The item index.</param>
		/// <returns>The position.</returns>
		
		var spacingSize = this.options.spacing * itemIndex;
		var existingItemSize = itemIndex * itemSize;
		return existingItemSize + spacingSize;
	},
	
	initializePages: function()
	{
		/// <summary>Initializes rows, columns, and items on the pages.</summary>
		
		//++initCounter;
		//this.control.trace("initCounter: " + initCounter);
		
		var reload = false;
		var columns = this.determineItemFit(this.options.itemWidth, this.root.width);
		var rows = this.determineItemFit(this.options.itemHeight, this.root.height);
		
		if (this.columns != columns || this.rows != rows)
		{
			reload = true;
			this.columns = columns;
			this.rows = rows;
			this.itemCountPerPage = columns * rows;
		}
		
		var pageWidth = this.columns * this.options.itemWidth + (this.columns - 1) * this.options.spacing;
		var pageHeight = this.rows * this.options.itemHeight + (this.rows - 1) * this.options.spacing;
		var centeringWidth = Math.max(this.root.width / 2 - pageWidth / 2, this.options.padding);
		var centeringHeight = this.options.padding;
		
		this.currentPage.width = this.nextPage.width = pageWidth;
		this.currentPage.height = this.nextPage.height = pageHeight;
		this.currentPage["Canvas.Left"] = this.nextPage["Canvas.Left"] = centeringWidth;
		this.currentPage["Canvas.Top"] = this.nextPage["Canvas.Top"] = centeringHeight;
		this.centerClip.Rect = centeringWidth + "," + centeringHeight + "," + pageWidth + "," + pageHeight;
		
		return reload;
	},
	
	showPage: function(items, animationDirection)
	{
		/// <summary>Displays the specified page of items.</summary>
		/// <param name="items">The items to display.</param>
		/// <param name="animationDirection">The animation direction (i.e. "Next" or "Previous").</param>
		
		var currentDuration = 0;
		var nextDuration = 0;
		var currentToValue = 0;
		var nextToValue = 0;
	
		if (animationDirection && this.options.animatePageChanges)
		{
			if (animationDirection == "Next")
			{
				currentDuration = (this.currentPageTransform.x + this.root.width) / this.root.width * this.options.animationDuration;
				nextDuration = (this.nextPageTransform.x + this.root.width) / this.root.width * this.options.animationDuration;
				currentToValue = -(this.currentPage.width + this.options.spacing);
				nextToValue = -(this.nextPage.width + this.options.spacing);
				this.nextPage["Canvas.Left"] = (this.currentPage["Canvas.Left"] + this.nextPage.width + this.options.spacing);
			}
			else
			{
				currentDuration = (this.root.width - this.currentPageTransform.x) / this.root.width * this.options.animationDuration;
				nextDuration = (this.root.width - this.nextPageTransform.x) / this.root.width * this.options.animationDuration;
				currentToValue = this.currentPage.width + this.options.spacing;
				nextToValue = this.nextPage.width + this.options.spacing;
				this.nextPage["Canvas.Left"] = (this.currentPage["Canvas.Left"] - this.nextPage.width - this.options.spacing);
			}
		}
		
		this.addItemsToContainer(this.nextPage, items);
		this.nextPage.visibility = "Visible";
		
		this.currentPageSplineFrom.value = this.currentPageTransform.x;
		this.currentPageSplineTo.value = currentToValue;
		this.currentPageSplineTo.keyTime = "0:0:" + currentDuration.toFixed(8);
		
		this.nextPageSplineFrom.value = this.nextPageTransform.x;
		this.nextPageSplineTo.value = nextToValue;
		this.nextPageSplineTo.keyTime = "0:0:" + nextDuration.toFixed(8);
		
		this.storyboard.begin();
	},
	
	addItemsToContainer: function(page, items)
	{
		/// <summary>Adds an array of items to a page.</summary>
		/// <param name="page">The page.</param>
		/// <param name="items">The items to add.</param>
		
		page.children.clear();
		
		for (var i = 0, j = 0; j < this.rows; j++)
		{
			for (var k = 0; k < this.columns; k++, i++)
			{
				if (items.length > i)
				{
					var item = items[i];
					page.children.add(item.root);
					
					item.setOptions(
					{
						width: this.options.itemWidth,
						height: this.options.itemHeight,
						top: this.determineCanvasPosition(this.options.itemHeight, j),
						left: this.determineCanvasPosition(this.options.itemWidth, k)
					});
										
					item.render();
				}
			}
		}	
	},
	
	loadPageByOffset: function(offset)
	{
		/// <summary>Displays a new page based on an offset from the current page index.</summary>
		/// <param name="offset">An offset from the current page index.</param>
		
		//++loadCounter;
		//this.control.trace("loadCounter: " + loadCounter + ", initCounter: " + initCounter);
		
		var itemCount = this.parent.getItemCount();
		
		if (itemCount > 0 && this.itemCountPerPage > 0)
			this.pageCount = Math.ceil(itemCount / this.itemCountPerPage);
		else
			this.pageCount = 0;
		
		var pageIndex = this.pageIndex + offset;
		
		if (pageIndex < 0)
			pageIndex = 0;
		
		if (pageIndex < this.pageCount)
		{
			this.pageIndex = pageIndex;
			var page = this.parent.getItems(this.pageIndex * this.itemCountPerPage, this.itemCountPerPage);
			var direction = (offset > 0) ? "Next" : (offset < 0) ? "Previous" : null;
			this.showPage(page, direction);
		}
		
		this.fireEvent("pageLoad");
	},
	
	refresh: function()
	{
		if (this.initializePages())
			this.loadPageByOffset(0);
	},
	
	onStoryboardComplete: function(sender, e)
	{
		/// <summary>Swaps the page instance references and resets the animation storyboard for the next page change.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		var nextPage = this.nextPage;
		var currentPage = this.currentPage;
		
		nextPage.visibility = "Visible";
		currentPage.visibility = "Collapsed";
		currentPage.children.clear(); 
		
		this.currentPage = this.nextPage;
		this.nextPage = currentPage;
		this.storyboard.stop();
		
		this.initializePages();
	},
	
	onSizeChanged: function()
	{
		/// <summary>Handles the event fired when the control is resized.</summary>
		
		SlideShow.PageContainer.base.onSizeChanged.call(this);
		
		this.pageIndex = 0;
		
		if (this.parent.root.visibility != "Collapsed")
		{
			window.clearTimeout(this.refreshTimerId);
			this.refreshTimerId = window.setTimeout(SlideShow.createDelegate(this, this.refresh), 10);
		}
	}
});

/*******************************************
 * class: SlideShow.SlideNavigation
 *******************************************/
SlideShow.SlideNavigation = function(control, parent, xaml)
{
	/// <summary>Provides a base class for navigation controls that interact with a SlideViewer instance.</summary>
	/// <param name="control">The Slide.Show control.</param>
	/// <param name="parent">The parent control.</param>
	/// <param name="xaml">The XAML for the control.</param>
	
	SlideShow.SlideNavigation.base.constructor.call(this, control, parent, xaml);
	
	SlideShow.merge(this.options,
	{
		enableNextSlide: true,
		enablePreviousSlide: true,
		enableTransitionOnNext: true,
		enableTransitionOnPrevious: false
	});
	
	this.control.addEventListener("modulesLoad", SlideShow.createDelegate(this, this.onControlModulesLoad));
};

SlideShow.extend(SlideShow.UserControl, SlideShow.SlideNavigation,
{
	slideExistsByOffset: function(offset)
	{
		/// <summary>Determines if a slide exists based on the specified offset.</summary>
		/// <param name="offset">An integer value representing an offset from the current slide index.</param>
		/// <returns>True if a slide exists based on the offset.</param>
	
		return (this.options.loopAlbum || this.control.isSlideIndexValid(this.slideViewer.currentAlbumIndex, this.slideViewer.currentSlideIndex + offset));
	},
	
	showPreviousSlide: function()
	{
		/// <summary>Shows the previous slide in the slideshow.</summary>
		
		var offset = -1;
		
		if (this.slideViewer.currentSlideIndex != this.slideViewer.getDataIndexByOffset(offset))
		{
			if (this.slideExistsByOffset(offset))
			{
				if (this.slideViewer.currentTransition && this.slideViewer.currentTransition.state == "Started")
					this.slideViewer.fromImage.setSource(this.slideViewer.toImage.image.source);
				
				this.slideViewer.loadImageByOffset(offset, this.options.enableTransitionOnPrevious);
			}
		}
	},
	
	showNextSlide: function()
	{
		/// <summary>Shows the next slide in the slideshow.</summary>
		
		var offset = 1;
		
		if (this.slideViewer.currentSlideIndex != this.slideViewer.getDataIndexByOffset(offset))
		{
			if (this.slideExistsByOffset(offset))
			{
				if (this.slideViewer.currentTransition && this.slideViewer.currentTransition.state == "Started")
					this.slideViewer.fromImage.setSource(this.slideViewer.toImage.image.source);

				this.slideViewer.loadImageByOffset(offset, this.options.enableTransitionOnNext);
			}
		}
	},
	
	onControlModulesLoad: function(sender, e)
	{
		/// <summary>Completes initialization after all modules have loaded.</summary>
		/// <param name="sender">The event source.</param>
		/// <param name="e">The event arguments.</param>
		
		this.slideViewer = this.control.modules["SlideViewer"];
		
		if (!this.slideViewer)
			throw new Error("Expected module missing: SlideViewer");
	}
});

/*******************************************
 * class: SlideShow.PageNavigation
 *******************************************/
SlideShow.PageNavigation = function(control, parent, xaml)
{
	/// <summary>Provides a base class for page container navigation.</summary>
	/// <param name="control">The Slide.Show control.</param>
	/// <param name="parent">The parent control.</param>
	/// <param name="xaml">The XAML for the control.</param>
	
	SlideShow.PageNavigation.base.constructor.call(this, control, parent, xaml);
};

SlideShow.extend(SlideShow.SlideNavigation, SlideShow.PageNavigation,
{
	showPreviousPage: function()
	{
		/// <summary>Shows the previous page in the page container.</summary>
		
		this.parent.pageContainer.loadPageByOffset(-1);
	},
	
	showNextPage: function()
	{
		/// <summary>Shows the next page in the page container.</summary>
		
		this.parent.pageContainer.loadPageByOffset(1);
	}
});