Welcome to Atalasoft Community Sign in | Help

UpdatePanel and Sys.Application.add_load Woes

I’ve found (as others have) an interesting problem with ASP.NET AJAX’s Sys.Application.add_load function.  It doesn’t quite work as expected in all browser environments, under certain conditions.

We have several ASP.NET based controls that use a lot of JavaScript on page load to initialize, and most of these controls are supposed to support being nested inside of an UpdatePanel.  For whatever reason, our composite controls’ initialization JavaScript manages to render after the call to Sys.Application.initialize.  Since our file based scripts load inside of the UpdatePanel, they are required to make calls to Sys.Application.notifyScriptLoaded as they load.

In Firefox 3, Safari 3 (on Windows), and Opera 9, the initialization scripts never run on the first load.  From my tests, I’ve concluded that Internet Explorer, Chrome, and FireFox 2 don’t have this problem.

Here are the basic conditions that cause this:

  1. Your script block renders after the Sys.Application.initialize block that ASP.NET injected into the page.
  2. You add your initialization script to the load event by using Sys.Application.add_load.
  3. You have at least one file based script file that calls Sys.Application.notifyScriptLoaded to notify the application that you’re waiting for scripts to load before you run your initialization script.

The solution that I’ve seen provided for edge cases such as this, states that you can define your initialization function anywhere on the page as shown:

function pageLoad(){
   // initialize your JavaScript here
}

From what I can tell, this works great for almost everyone that encounters this problem.  The documentation for this reserved named function is located in the ASP.NET AJAX Client Life-Cycle Events section.

This won’t help us unfortunately, since our controls are being used by developers that may need to use the same method.  Defining a pageLoad function on the page where someone else has already defined it will basically nullify the other function of that name.  So where does that leave us?  The DOM.

You may be thinking that I mean window.onload, and that’s sort of correct.  This event fires when the page is finished loading, but it only fires once, and doesn’t fire for every partial postback that the UpdatePanel does.  By using Sys.UI.DomEvent.addHandler, we can add an event handler that behaves the same way as Sys.Application.add_load.  Here’s the code:

function yourInit(){
   // initialize your JavaScript here
}
if (typeof(Sys) !== 'undefined'){
   Sys.UI.DomEvent.addHandler(window, "load", yourInit);
}
else{
   // no ASP.NET AJAX, use your favorite event
   // attachment method here
}

In my tests, this made the initialize event fire in the correct manner for all of the browsers that I tested.

If you have experience with this problem, and you found another way around it… please feel free to comment below.  Thanks!

Internet Explorer TextArea scrollHeight Bug

While working with some dynamically created textarea tags, I noticed some odd behavior in Internet Explorer.  I was trying to get a textarea tag to fit to the height of its content, while allowing a horizontal scrollbar for when a word (or url) is too long for the available width.

Here is the function I came up with:

function fitTextToHeight(textarea){
   // get rid of scrollbars to get accurate height
   textarea.style.overflow = 'hidden';
   // set height to available scroll height
   textarea.style.height = textarea.scrollHeight + 'px';
   // allow horizontal scrolling if words are too long
   textarea.style.overflowX = 'auto';
   // keep the vertical scrollbar hidden, as it's no longer needed
   textarea.style.overflowY = 'hidden';
}

This function works great for all browsers that I tested (Firefox, Chrome, Safari, Opera) except Internet Explorer.  I tested it a bit further, to see how far back this bug goes, and I was able to reproduce it in IE6 through IE8 (At the time of this article v8.0.6001.18702).  IE 5.5 behaved as the other browsers did.

To work around this issue, I noticed that just making a dummy statement for scrollHeight, makes it behave as expected.  Work around shown below:

function fitTextToHeight(textarea){
   // get rid of scrollbars to get accurate height
   textarea.style.overflow = 'hidden';
   // Internet Explorer 6-8 needs this to get the correct value
   textarea.scrollHeight;
   // set height to available scroll height
   textarea.style.height = textarea.scrollHeight + 'px';
   // allow horizontal scrolling if words are too long
   textarea.style.overflowX = 'auto';
   // keep the vertical scrollbar hidden, as it's no longer needed
   textarea.style.overflowY = 'hidden';
}

Here’s a demo: Internet Explorer TextArea scrollHeight Bug Demo

One other thing that I noticed, is that you can put an alert right before using the scrollHeight property, and it works fine.  So this is not necessarily just tied to accessing the scrollHeight property, it may have something to do with execution time.

Internet Explorer 8 IFrame Height Nesting Bug

While testing our components and demos in the latest Internet Explorer 8 release (RC1), I found that a particular combination of nesting percentage based elements, caused an IFrame that we use to size to the default height, rather than filling the parent element.

The problem seems to only manifest when a div tag that uses both absolute based width (ex: 170px), and percentage based height (ex: 100%), is nested inside a table with absolute width and height.

Here’s the xhtml that breaks it (the tag in bold is causing the height problem):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>IE 8 IFrame Height Nesting Bug Demo</title>
</head>
<body>
  <table border="0" cellpadding="0" cellspacing="0" style="width:170px; height:494px;">
  <tr><td style="width:170px; height:494px;">
      <div id="container_div" style="width:170px; height:100%;">
        <table cellspacing="0" cellpadding="0" border="0" style="width:100%; height:100%;">
        <tr><td style="width:100%; height:100%; background-color:Green;">
            <iframe frameborder="1" marginwidth="0" marginheight="0" style="width:100%; height:100%;" src="spacer.gif"></iframe>
            </td>
        </tr>
        </table>
      </div>
  </td></tr>
  </table>
</body>
</html>

To work around the problem in this case, I was able to simply set the width to 100%:

      <div id="container_div2" style="width:100%; height:100%;">
         <table cellspacing="0" cellpadding="0" border="0" style="width:100%; height:100%;">
         <tr><td style="width:100%; height:100%; background-color:Green;">
             <iframe frameborder="1" marginwidth="0" marginheight="0" style="width:100%; height:100%;" src="resources/mask.png"></iframe>
             </td>
         </tr>
         </table>
      </div>

Here is a side by side comparison of the two snippets: IE 8 IFrame Height Nesting Bug Demo

In case you don’t have IE8 installed, here is a screenshot:
IE8IFrameHeight

Posted by David Cilley | 1 Comments
Filed under:

Introducing AJAX based Freehand, Polygon, and Lines Annotations

new_annosIf you haven’t already made it over to our DotImage 7.0 AJAX Annotations Demo, I  encourage you to take a look.  This demonstrates our new JavaScript annotation editors, which are based on the client side Canvas object.  We’re keeping with our zero-footprint tradition, and supporting most browsers: Internet Explorer 6-8, Firefox 2-3, Apple Safari, Google Chrome, and Opera 9. This demo was created using DotImage 7.0, DotImage Advanced Document Cleanup (ADC), and YUI (for the dialogs, right click menu, color picker, and animation).

Here’s what’s new:

Annotations that can now be created with the mouse:

  • Freehand
  • Lines
  • Polygon

These annotations have had their client side editors improved:

  • Ellipse
  • Rectangle (Including highlighter, redaction, etc)
  • Line

We’ve also made several improvements to the WebThumbnailViewer, allowing it to load large numbers of thumbnails without affecting performance.

Here is a more detailed list of DotImage 7.0 new features.

Posted by David Cilley | 0 Comments
Filed under: , , ,

Converting YUI ButtonGroups into Image Buttons

ButtonGroups in YUI are fairly quick to implement, have a nice look, and act the way most people expect.  I’ve been surprised to find out that there are no options available to use images in place of the text on the Buttons inside the ButtonGroup.

In our most recent AJAX Photo Viewer Demo, I decided to use a ButtonGroup for the main mouse tool controls, and Bill Bither suggested that I use icons instead of text.

I didn’t want to throw away what I’d done so far, since the demo was pretty much done at that point.  I poked around YUI’s documentation a bit and came up with simple function that can convert a YUI Button into an image only button, much like a button on a tool bar.  These converted YUI Buttons still maintain the same original properties internally, so you can use them interchangeably.

Here’s a quick demonstration:

Here’s what you need:
YUI Dependencies:

<!-- Combo-handled YUI CSS files: -->  
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/combo?2.6.0/build/button/assets/skins/sam/button.css" />
<!-- Combo-handled YUI JS files: -->
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.6.0/build/yahoo-dom-event/yahoo-dom-event.js&2.6.0/build/element/element-beta-min.js&2.6.0/build/button/button-min.js"></script>

CSS:

/* Our new icon class */
.icon
{
	background-repeat: no-repeat;  
	background-position: 2px 2px;
	cursor: pointer;
	overflow: hidden;
	height: 28px;
	width: 28px;
}

HTML:

<div id="mainContainer">
	<!-- This is where the button bar will be created -->
</div>
<div style="padding-top: 10px;">
	<input type="button" value="Convert to Image Buttons" onclick="return onConvertClick();" />
</div>

Java Script:

var _mainButtonGroup = null;
// An array of arrays containing the label/value of each button, and its corresponding tooltip
// The label of each of these buttons will be used as the base filename for the icon.
var _mainButtonData = [
	['Pan', 'Left click and drag to pan the image.'],
	['Center', 'Left click anywhere on the image to re-center the control\'s position.'],
	['Selection', 'Click and drag to make a rectangular selection. Click outside the selection to remove it.'],
	['Ellipse', 'Click and drag to make a rectangular selection. An ellipse will be rendered inside the selected area.'],
	['Rectangle', 'Click and drag to make a rectangular selection. A rectangle will be rendered inside the selected area.'],
	['Text', 'Click and drag to make a rectangular selection. Sample text will be rendered inside the selected area.'],
	['Image', 'Click and drag to make a rectangular selection. An image will be rendered inside the selected area.'],
	['Zoom', 'Left click anywhere on the image to zoom in, right click to zoom out.'],
	['ZoomSelection', 'Click and drag to make a rectangular selection. The area you select will be zoomed into.']
]
		
// Create the button group when the page is ready
YAHOO.util.Event.onDOMReady(onPageLoad);
function onPageLoad(){
	createButtonGroup();
}
		
function createButtonGroup(){
	// Create new YUI button group on the 'mainContainer' div
	_mainButtonGroup = new YAHOO.widget.ButtonGroup({id:'mainButtonGroup', container: 'mainContainer'});
	// Add some buttons to the group
	for (var i = 0; i < _mainButtonData.length; i++){
		_mainButtonGroup.addButton({label: _mainButtonData[i][0], value: _mainButtonData[i][0] });
	}
			
	// Render the new button group
	_mainButtonGroup.render();
}
		
// Simple demo function that runs the converter function
function onConvertClick(){
	var buttons = _mainButtonGroup.getButtons();
			
	// Loop through the Button array and convert each button
	for (var i = 0; i < buttons.length; i++){
		convertButtonToImageButton(buttons[i], _mainButtonData[i][1]);
	}
}
// Main converter
function convertButtonToImageButton(button, tooltip){
	// We use the value of the the button for the icon name
	var name = button.get('value');
	// Use the icon as a background image
	// It's entirely possible for you to use the CSS sprite method here, by 
	// incrementing the background offset rather than using different names
	button._button.parentNode.style.backgroundImage = 'url(icons/' + name + '.png)';
	// Add our new icon class to the existing classes
	button._button.parentNode.className += ' icon';
	// Add tooltip to the parent node
	button._button.parentNode.title = tooltip;
	// Hide the text of the button
	button._button.style.visibility = 'hidden';
}
Posted by David Cilley | 1 Comments
Filed under: ,

Using YUI Test to unit test Atalasoft WebControls

Yesterday I read an informative article on how to get started using YUI Test to write unit tests for JavaScript, by Nicholas C. Zakas.  After reading this article, I decided to give it a try, by testing some functions of the WebImageViewer and the WebThumbnailViewer controls.

It took me all of about 15 minutes to write this demo, while following the instructions, and a few more minutes polishing it up for this post.  I am really impressed how easy YUI Test is to work with, and how quickly I was able to get up and running.

Kudos to the YUI team!

Here's a link to my online YUI Test Unit Testing Demo, and here's the code:

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
  <title>YUI Test Unit Testing Demo</title>
  <!--CSS-->
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.6.0/build/logger/assets/logger.css" />
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.6.0/build/yuitest/assets/testlogger.css" />

<!-- Combo-handled YUI JS files: -->
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.6.0/build/yahoo-dom-event/yahoo-dom-event.js&2.6.0/build/logger/logger-min.js&2.6.0/build/yuitest/yuitest-min.js"></script>
</head>
<body>
  <form id="form1" runat="server">
  <div>Using YUI Test to Unit Test Atalasoft WebControls</div>
  <div style="height:800px;">
  <table border="0" cellpadding="0" cellspacing="2" style="width:100%; height:100%">
  <tr><td colspan="2" style="width:100%; height:36px;">
    <input type="button" onclick="OpenTestImage(); return false;" value="Open Image and run tests"  />
 </td></tr>
  <tr><td style="width:200px; height:100%; border: 1px solid black; background-color:Silver">
    <cc1:WebThumbnailViewer ID="WebThumbnailViewer1" runat="server"
     Width="150px"
     Height="100%"
     TitleBar="Thumbs"
     ViewerID="WebImageViewer1"
     Centered="true" />
   </td><td style="width:100%; height:100%; border: 1px solid black; background-color:Silver">
   <cc1:WebImageViewer ID="WebImageViewer1" runat="server"
     Width="100%"
     Height="100%"
     AutoZoom="BestFit"
     AntialiasDisplay="ScaleToGray"
     TitleBar="Image" />
</td></tr>
   </table>
    </div>
<script language="javascript" type="text/javascript">
  var _testUrl = 'Images/DocCleanMultipage.tif';
  var _testIndex = 1;
  var _logger = null;
  
  // Runs once the page has finished loading
  atalaInitClientScript(Init);
  function Init(){
    InitEvents();
    InitTests();
  }
  
  // Bind our event handlers
  function InitEvents(){
    WebThumbnailViewer1.UrlChanged = UrlLoaded;
    WebThumbnailViewer1.SelectedIndexChanged = IndexChanged;
   
    // We use ImageSizeChanged because this event fires when
    // a callback is returned from the server, ImageChanged fires immediately
    WebImageViewer1.ImageSizeChanged = ImageLoaded;
  }
  
  // Set up the test functions
  function InitTests(){
    var testCase = new YAHOO.tool.TestCase({
     name: "Open Image Tests",
     testUrlLoaded : function(){
      // tests whether the url given is the one that was loaded in the WebThumbnailViewer
      YAHOO.util.Assert.areEqual(_testUrl, WebThumbnailViewer1.getUrl());
     },
    testSelectedIndex : function(){
      // tests that the index given is returned as being selected in the WebThumbnailViewer
     YAHOO.util.Assert.areEqual(_testIndex, WebThumbnailViewer1.getSelectedIndex());
    },
    testImageLoaded : function(){
      // tests whether the url given is the one that was loaded in the WebImageViewer
      YAHOO.util.Assert.areEqual(_testUrl, WebImageViewer1.getImageUrl());
   },
     testFrameIndex : function(){
     // tests that the given selected index was the index loaded into the WebImageViewer
      YAHOO.util.Assert.areEqual(_testIndex, WebImageViewer1.getFrameIndex());
    }
   });
   
 // Add the test case to the test runner
    YAHOO.tool.TestRunner.add(testCase);
   
    // Create the logger (shows it too)
    _logger = new YAHOO.tool.TestLogger("testLogger");
  }
  
  // Select a thumbnail once the image has been loaded into the thumbnailviewer
  function UrlLoaded(){
    WebThumbnailViewer1.SelectThumb(_testIndex);
  }
  
  // Change the test's selected index, so that clicking on a thumb will test that index
  function IndexChanged(){
    _testIndex = WebThumbnailViewer1.getSelectedIndex();
  }
  
  // We want to run the tests after the callback from the server
  // If this were an automated test, this should have a time limit
  // so we can run the tests even if there is no response from the server
  function ImageLoaded(){
    RunTests();
  }
  
  // Run the tests
  function RunTests(){
    YAHOO.tool.TestRunner.run();
  }
  
  // Open the test image
  function OpenTestImage(){
    WebThumbnailViewer1.OpenUrl(_testUrl);
  }
 </script>
</form>
</body>
</html>

Posted by David Cilley | 2 Comments

Ajax Image Sliders Part 3: Intervals with Opacity

This is Part 3 of a multi-part blog series.

  1. The OnDemand method
  2. The Interval method
  3. The Interval Opacity method

On the previous two slider examples, I used a YUI slider that had a range from -100 to 100, with a total of 201 possible values.  Both of these examples still have some disconnect from the action performed, and still feel like we're on the web.  I promised that we could improve upon this, and I don't mean by adding more intervals... there's one more browser trick we can use to get this right.

The Interval Opacity Method
This is possibly the most responsive method for using sliders with images in the native browser.  This method builds upon the Interval method by using browser native opacity to 'create' the missing steps between the intervals that we've already downloaded.  This also removes the OnDemand portion that was causing the flicker in Part 2.  Two image tags are shown in this method, one on top of the other, the the top one partially opaque depending on the percentage between intervals.

Without changing the server side code of Part 1, and making a few changes to the client side code that we've built in Part 2, we get a visual update for every available value of the slider.  As of this posting, this method works in IE 5.5/6/7, Mozilla Firefox, Safari, and Opera.

Here's the demo:

Here's what you need:

  1. An input tag to hold/edit the value (I used a text box)
  2. A JavaScript slider control (YUI slider in this example)
  3. Some JavaScript event handlers
  4. A server side method that returns an updated image from a querystring containing a path and a change value
  5. A loop that creates a series of img tags on page load, and pre-populates them with images (client side)
  6. A method that shows and hides the tags as they are needed (client side)
  7. A method that shows partially opaque images, using style.opacity or style.filter = alpha(value) for IE

The server side code for this example is attached the Part 1 article, I have provided the client-side code inline:

 <!-- YUI Dependencies -->
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/yahoo-dom-event/yahoo-dom-event.js%22%3E%3C/script>
 <script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/animation/animation-min.js%22%3E%3C/script>
 <script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/dragdrop/dragdrop-min.js%22%3E%3C/script>
 <script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/slider/slider-min.js%22%3E%3C/script>
 
 <!-- XHTML -->
 <div>
   <div id="container"><asp:Image ID="Image1" runat="server" ImageUrl="images/spacer.gif" Width="320" Height="240" /></div>
   <div style="padding-left:6px;">Gamma</div>
   <div id="sliderbg" style="position:relative; background:url(images/bg-fader.gif) 5px 0 no-repeat; height:28px; width:228px;">
    <div id="sliderthumb" style="position: absolute; top: 4px;"><img src="images/thumb-n.gif" /></div>
   </div>
  <input id="gammaVal" maxlength="4" size="4" type="text" value="0" style="position:relative; left:230px; top:-24px;" />
 </div>
 <script type="text/javascript">
var _slider;
var _gammaVal = document.getElementById('gammaVal');
var _url = 'Images/Rosebud.jpg'; // Starting image url
var _path = '<%= this.Page.Request.CurrentExecutionFilePath %>'; // url path of this page
 
var _imgs; // Array used to hold the img tags used for the images
var _steps = 20;
var _range = 200;
var _container = document.getElementById('container');
var _showing = 0;
	function init(){
_slider = YAHOO.widget.Slider.getHorizSlider("sliderbg", "sliderthumb", 0, _range);
_slider.subscribe('change', chgInterval); // Updates the text field and the image

_imgs = new Array();
// pre loads the images se we don't have to wait for them to load
for (var g = 0; g <= _steps ; g++){
var i = new Image();
i.src = _path + '?img=' + _url + '&gamma=' + (g * (_range / _steps));
i.style.left = '0px';
i.style.top = '0px';
i.style.position = 'absolute';
i.style.visibility = 'hidden';

_container.appendChild(i);
_imgs.push(i);
}
		YAHOO.util.Event.on(_gammaVal, "blur", checkValue);
YAHOO.util.Event.on(_gammaVal, "keydown", keyDown);
checkValue();
}

YAHOO.util.Event.onDOMReady(init);
	// Changes the text field value
function chgValue(x){
_gammaVal.value = x - 100;
}
	// hides the image at the given interval, and the image overlaying it (if any)
function hide(i){
_imgs[i].style.visibility = 'hidden';
		if (i + 1 < _imgs.length){
_imgs[i + 1].style.visibility = 'hidden';
}
}

// shows the image at the given interval, and the image overlaying it (if any)
function show(i){
_imgs[i].style.visibility = 'visible';

if (i + 1 < _imgs.length){
_imgs[i + 1].style.visibility = 'visible';
}
}
	// Changes the shown img tag
function chgInterval(x){
var shownum = Math.floor(x/(_range/_steps));
var opacity = Math.round(((x/(_range/_steps)) - shownum) * 100);
		if (shownum != _showing){
hide(_showing);
show(shownum);
_showing = shownum;
}

// set the opacity of the overlaying image, to a percentage
if (_showing + 1 < _imgs.length){
setOpacity(_imgs[_showing + 1], opacity);
}

setOpacity(_imgs[_showing], 100);
		chgValue(x);
}

// sets the percentage based opacity (0-100) of the given object
function setOpacity(obj, opacity){
obj.style.opacity = (opacity / 100);
obj.style.filter = 'alpha(opacity=' + opacity + ')';
}

// Checks the value of the text field, and sets the slider to that value
function checkValue(){
i = parseInt(_gammaVal.value);
		if (isNaN(i)){
i = _slider.getValue() - 100;
}

_slider.setValue(i + 100, false);
}

// Used to determine if the enter key has been pressed
function keyDown(k){
if (k.keyCode == 13){
checkValue();
return false;
}
}
</script>

Posted by David Cilley | 2 Comments
Filed under: , , ,

Ajax Image Sliders Part 2: Intervals with On Demand

This is Part 2 of a multi-part blog series.

  1. The OnDemand method
  2. The Interval method
  3. The Interval Opacity method

On the previous slider example, I used a YUI slider that had a range from -100 to 100.  This is a total of 201 different combinations for one image dialog, and that's about 10-20 times more requests than a web server should have to handle in a reasonable amount of time.  We want to make this look as if the slider is actually changing the image while we scroll it, but we don't want to request 201 images up front, and we don't want to load them all on demand either.

The Interval Method
This method requests a series of images from the server, at a set interval along the entire slider.  These images are requested as the page is loading, new requests are made when the slider has finished moving.  The pre-loaded images are stored in their own image tags, hidden and shown when needed.  This is along the same lines as many image pre-load scripts that were used for rollovers, many years ago.

Without changing the server side code of Part 1, I was able to use this method to create a slider that looks dynamic enough to fool most people into thinking it's changing it on the fly.  Because the main image tag is updated to hold the new image when the drag thumb is dropped, there can be a slight flicker.  We'll see if we can take care of that in Part 3. 

Here's the demo for this method:

Here's what you need:

  1. An img tag
  2. An input tag to hold/edit the value (I used a text box)
  3. A JavaScript slider control (YUI slider in this example)
  4. Some JavaScript event handlers
  5. A server side method that returns an updated image from a querystring containing a path and a change value
  6. A loop that creates a series of img tags on page load, and pre-populates them with images (client side)
  7. A method that shows and hides the tags as they are needed (client side)

The server side code for this example is attached the Part 1 article, I have provided the client-side code inline:

 <!-- YUI Dependencies -->  
 <script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/yahoo-dom-event/yahoo-dom-event.js%22%3E%3C/script>
 <script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/animation/animation-min.js%22%3E%3C/script>
 <script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/dragdrop/dragdrop-min.js%22%3E%3C/script>
 <script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/slider/slider-min.js%22%3E%3C/script>
 
 <!-- XHTML -->
 <div>
  <div id="container"><asp:Image ID="Image1" runat="server" ImageUrl="images/spacer.gif" Width="320" Height="240" /></div>
  <div style="padding-left:6px;">Gamma</div>
  <div id="sliderbg" style="position:relative; background:url(images/bg-fader.gif) 5px 0 no-repeat; height:28px; width:228px;">
   <div id="sliderthumb" style="position: absolute; top: 4px;"><img src="images/thumb-n.gif" /></div>
  </div>
  <input id="gammaVal" maxlength="4" size="3" type="text" value="0" style="position:relative; left:230px; top:-24px;" />
 </div>
<script type="text/javascript">
 var _slider;
 var _gammaVal = document.getElementById('gammaVal');
 var _img = document.getElementById('<%= this.Image1.ClientID %>');  // ASP.NET server control is used for server side access
 var _url = 'Images/Rosebud.jpg'; // Starting image url
 var _path = '<%= this.Page.Request.CurrentExecutionFilePath %>'; // url path of this page
 
 var _imgs; // Array used to hold the img tags used for the images
 var _steps = 20;
 var _range = 200;
 var _container = document.getElementById('container');
 var _showing = 0;
 function init(){
  _slider = YAHOO.widget.Slider.getHorizSlider("sliderbg", "sliderthumb", 0, _range);
  _slider.subscribe('change', chgInterval); // Updates the text field and the image
  _slider.subscribe('slideEnd', chgImg); // Updates the image
  
  _imgs = new Array();
  // pre loads the images se we don't have to wait for them to load
     for (var g = 0; g <= _steps ; g++){
   var i = new Image();
    i.src = _path + '?img=' + _url + '&gamma=' + (g * (_range / _steps));
    i.style.left = '0px'; 
    i.style.top = '0px';
    i.style.position = 'absolute';
    i.style.visibility = 'hidden';
   
   _container.appendChild(i);
   _imgs.push(i);
  }
        YAHOO.util.Event.on(_gammaVal, "blur", checkValue);
        YAHOO.util.Event.on(_gammaVal, "keydown", keyDown);
  checkValue();
 }
 
 YAHOO.util.Event.onDOMReady(init);
 // Changes the text field value
 function chgValue(x){
  _gammaVal.value = x - 100;
 }
 // Changes the url of the img tag
 function chgImg(){
  _img.src = _path + '?img=' + _url + '&gamma=' + (parseInt(_gammaVal.value) + 100);
  hideShowing();
 }
 
 function hideShowing(){
  _imgs[_showing].style.visibility = 'hidden';
 }
 // Changes the shown img tag
 function chgInterval(x){
  var shownum = Math.round(x/(_range/_steps));
  if (shownum != _showing){
   hideShowing();
   
   _showing = shownum;
   _imgs[_showing].style.visibility = 'visible';
  }
  chgValue(x);
 }
 
 // Checks the value of the text field, and sets the slider to that value
 function checkValue(){
  i = parseInt(_gammaVal.value);
  if (isNaN(i)){
   i = _slider.getValue() - 100;
  }
   
  _slider.setValue(i + 100, false);
 }
 
 // Used to determine if the enter key has been pressed
 function keyDown(k){
  if (k.keyCode == 13){
   checkValue();
   return false;
  }
 }
</script>
Posted by David Cilley | 7 Comments
Filed under: , , ,

AJAX Image Sliders: Part 1

This is Part 1 of a multi-part blog series.

  1. The OnDemand method
  2. The Interval method
  3. The Interval Opacity method

One of the most common dialogs in an image editor application is the slider with preview.  When you move these applications over to the Web, you end up losing some of the user experience because of the asynchronous nature of these apps.

There are several ways to use a JavaScript slider to change an image, and I will be covering at least 3 of them over the next few posts.

The "OnDemand" method:
This is probably the easiest to implement, but least user friendly.  I say it's not user friendly because the image only updates after the slider thumb has finished moving.  If we were to make the slider update for all points, it would innundate the server with requests.  The usage of a slider, however, is still an improvement over a postback or button/text field combination.  The feedback from the text box helps the user see that something is happening as well.

Here's a demonstration:

Here's what you need:

  1. An img tag
  2. An input tag to hold/edit the value(I used a text box)
  3. A JavaScript slider control (YUI slider in this example)
  4. Some JavaScript event handlers
  5. A server side method that returns an updated image from a querystring containing a path and a change value

The server side code for this example is attached to this article (if you need it), I have provided the client-side code inline:

<!-- YUI Dependencies -->  
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/yahoo-dom-event/yahoo-dom-event.js%22%3E%3C/script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/animation/animation-min.js%22%3E%3C/script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/dragdrop/dragdrop-min.js%22%3E%3C/script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.1/build/slider/slider-min.js%22%3E%3C/script>
 
<!-- XHTML -->
<div>
  <div><asp:Image ID="Image1" runat="server" ImageUrl="images/spacer.gif" /></div>
  <div style="padding-left:6px;">Gamma</div>
  <div id="sliderbg" style="position:relative; background:url(images/bg-fader.gif) 5px 0 no-repeat; height:28px; width:228px;"> 
   <div id="sliderthumb" style="position: absolute; top: 4px;"><img src="images/thumb-n.gif" /></div>
  </div>
  <input id="gammaVal" maxlength="4" size="3" type="text" value="0" style="position:relative; left:230px; top:-24px;" />
</div>

<script type="text/javascript">
 var _slider;
 var _gammaVal = document.getElementById('gammaVal');
 var _img = document.getElementById('<%= this.Image1.ClientID %>');  // ASP.NET server control is used for server side access
 var _url = 'Images/Rosebud.jpg'; // Starting image url
 var _path = '<%= this.Page.Request.CurrentExecutionFilePath %>'; // url path of this page
 
 function init(){
  _slider = YAHOO.widget.Slider.getHorizSlider("sliderbg", "sliderthumb", 0, 200);
  _slider.subscribe('change', chgValue); // Updates the text field while the value is changing
  _slider.subscribe('slideEnd', chgImg); // We only want to change the image after we've finished sliding

        YAHOO.util.Event.on(_gammaVal, "blur", checkValue);
        YAHOO.util.Event.on(_gammaVal, "keydown", keyDown);
  checkValue();
 }
 
 YAHOO.util.Event.onDOMReady(init);
 // Changes the text field value
 function chgValue(x){
  _gammaVal.value = x - 100;
 }

 // Changes the url of the img tag
 function chgImg(){
  _img.src = _path + '?img=' + _url + '&gamma=' + (parseInt(_gammaVal.value) + 100);
 }
 
 // Checks the value of the text field, and sets the slider to that value
 function checkValue(){
  i = parseInt(_gammaVal.value);
  if (isNaN(i)){
   i = _slider.getValue() - 100;
  }
   
  _slider.setValue(i + 100, false);
 }
 
 // Used to determine if the enter key has been pressed
 function keyDown(k){
  if (k.keyCode == 13){
   checkValue();
   return false;
  }
 }

</script>
Posted by David Cilley | 4 Comments
Filed under: , , ,

Attachment(s): Slider_OnDemand.aspx.cs

Non-Rectangular Masks on the Web: Part 1

Over the past 15 or so years, I've edited and created a thousands of images.  I almost always use a mask for something, and it's very rarely only rectangular. I have a need for doing this on the web, natively in the browser, and I might not be the only one.

With JavaScript and the DOM, you can create a series rectangles that represent masks, but as the complexity of these masks go up, the number of DOM objects that need to be created go up.  This can make it slow to render, and slow to update.

Most of the browsers used today show PNG images with an alpha channel(transparency) correctly.  PNG images with an alpha channel can be used to mimic a client side mask by placing them on top of the object that you want to mask.  The image to the right is one of these PNG images, with a JPG image behind it. There are three things you need to do to create a masking PNG image:

  1. Create an image with a background color of your choice, at the size of the object you are masking.
  2. Make the areas you want selected completely transparent on the alpha channel (Black).
  3. Make the rest of the image half transparent on the alpha channel (Gray).

The remaining parts that you will need will be some JavaScript mouse events that allow you to drag a box on the object (something like a selection marquee), and a server side method that will create the PNG image described above.

Since I have access to DotImage, I can use the WebAnnotationViewer and RemoteInvoke to do everything.  In my case I decided to use a ReferencedImage annotation to hold the alpha PNG.  Here's a basic run down of the concept, using DotImage:

  1. Use the WebAnnotationViewer's Selection box to select an area on the image, and attach a JavaScript event to the Selection.Changed.  (MaskSelection In this example)
  2. In the Selection.Changed JavaScript event, use RemoteInvoke to run a Page level server side method that creates the masked PNG image from the selection box. (Remote_MaskSelection in this example)
  3. The server side method keeps two images in the disk cache.  One for the masked areas drawn, and one for the transparent PNG that represents those masked areas.
  4. Every time the server side method is called it overwrites these images with the new data from selection rectangle of the WebAnnotationViewer, effectively appending what was selected to the new mask.
  5. The mask image is an 8-bit grayscale which starts out completely gray (color index 127) to indicate half transparent.  Areas that are selected are drawn onto this image as black (index 0).
  6. The alpha PNG image is a 32-bit RGBA image with a background color (red in this case), and a SetAlphaFromMaskCommand is used to put the mask image into the alpha channel of this new PNG
  7. The alpha PNG is then added to an annotation on the server side, and the client side JavaScript is notified that it needs to update the annotation.

Here is the code I used to create the mask image and the alpha PNG:

 private void SynchronizeMaskAnnotation(Size size)
 {
   // This annotation was created on PageLoad, and is the only one on layer 0
   AnnotationUI ann = this.WebAnnotationViewer1.Annotations.GetAnnotation(0, -1, 0);
   
   ReferencedImageAnnotation refAnn = ann as ReferencedImageAnnotation;
   if (refAnn != null)
   {
     refAnn.Size = size;

     ReferencedImageData refAnnData = refAnn.Data as ReferencedImageData;
     if (refAnnData != null)
     {
       if (refAnnData.ImageObject != null)
       {
         ((Bitmap)refAnnData.ImageObject).Dispose();
         GC.Collect(); // This is done because the .NET Bitmap class doesn't free the file in time for us to write over it
       }
     }
   }

   this.WebAnnotationViewer1.UpdateAnnotations();
 }

 private void AppendMask()
 {
   string _pathToCache = System.Configuration.ConfigurationManager.AppSettings["AtalasoftWebControls_Cache"];
   string _prefix = Page.Session.SessionID + "_";
   string _pathToMask = this._pathToCache + _prefix + "Mask.png";
   string _pathToMaskAnn = this._pathToCache + _prefix + "MaskAnn.png";

   AtalaImage maskImage = null;
   AtalaImage alphaPNG = null;

   try
   {
     if (File.Exists(Page.MapPath(_pathToMask)))
       maskImage = new AtalaImage(Page.MapPath(_pathToMask));
     else
       maskImage = new AtalaImage(this.WebAnnotationViewer1.Image.Size.Width, this.WebAnnotationViewer1.Image.Size.Height, PixelFormat.Pixel8bppGrayscale, Color.Gray);

     alphaPNG = new AtalaImage(maskImage.Width, maskImage.Height, PixelFormat.Pixel32bppBgra, Color.Red);

     Canvas maskCanvas = new Canvas(maskImage);
     maskCanvas.DrawRectangle(this.WebAnnotationViewer1.Selection.Rectangle, new SolidFill(Color.Black));

     SetAlphaFromMaskCommand alpha = new SetAlphaFromMaskCommand(maskImage, false, AlphaMergeType.Replace);
     ImageResults results = alpha.Apply(alphaPNG);
     if (alphaPNG != results.Image)
     {
       alphaPNG.Dispose();
       alphaPNG = results.Image;
     }

     SynchronizeMaskAnnotation(alphaPNG.Size);

     maskImage.Save(Page.MapPath(_pathToMask), new PngEncoder(), null);
     alphaPNG.Save(Page.MapPath(_pathToMaskAnn), new PngEncoder(), null);
   }
   finally
   {
     if (maskImage != null)
       maskImage.Dispose();
     if (alphaPNG != null)
       alphaPNG.Dispose();
   }
 }
Posted by David Cilley | 1 Comments
Filed under: , ,

Atalasoft at AJAXWorld 2008 East

This year's AjaxWorld Conference & Expo was somewhat of a disappointment.  After experiencing the problems that I outlined last year, I anticipated a better overall conference.  They did appear to take a few things into consideration, such as the lunch breaks (this time you might have been able to sit down), and the communication of the free wireless access in the ballroom.  Very few of the sessions I attended were informative beyond scratching the surface of a particular topic or product.  I felt like this conference was still trying to sell me the idea of using AJAX, rather than show us what we (as an AJAX community) have been able accomplish since the last conference.  Here's a run down of the problems I saw with this conference, in frustration level order (1 being the most frustrating):

  1. This venue is too small.  Nearly every single session room was too small for the number of attendees that wanted to attend in any particular session (that I went to).  I missed three potentially interesting sessions because I couldn't even get into the room.  The sessions where I made it into a seat, still had anywhere from 5-20 people standing in the doorways or sitting on the floor.
  2. Wireless access was still not communicated, but we knew to ask about it this time.  There was no way to connect to the internet unless you were in the grand ballroom, most sessions were not in this room.
  3. If you didn't get to lunch or the snack break within the first 5-10 minutes, you might not get any food.
  4. Even though they improved the size of the dining area, there were many people standing and waiting for tables, even some sitting on the floor. 
  5. Most session slides were provided online, and not given to us beforehand on a cd (like last year).  Without wireless access, this was nearly useless.
  6. They did not enforce the badges at all, everything was open to everyone, regardless of what ticket you paid for. (except meals)

Sessions I attended:
Day 1:

  • Comet: The Web That's Instantly On and Always On by Jonas Jacobi (Kaazing): This was a fairly vague session on Comet.  Jonas went over the Bayeux protocol, which is used to do server push(s) to a client browser without plugins.
  • ASP.NET AJAX Design & Development Patterns by Joe Stagner (Microsoft): This was a great overview on ASP.NET and ASP.NET AJAX design patterns. Joe explained that the UpdatePanel is not always the best way to make an AJAX app, even though it is one of the easiest, because it sends the entire page state back and forth from the server.  He covered several models, two of which I found interesting: The Service Model, and the After Processing Model.  I recommend taking a look at his slides, which he has provided in his blog.
  • Improving ASP.NET User Interfaces with the AJAX Control Toolkit by Robert Boedigheimer (The Schwan Food Company): This was a good overview on the ASP.NET AJAX Control Toolkit.  He demonstrated several of the controls, but since there was no internet access, he couldn't demonstrate everything that he wanted to.

Day 2:

  • Can We Fix the Web? by Douglas Crockford (Yahoo!): This was the keynote I was looking forward to, Douglas usually has something good to say.  He discussed that security is the main problem with the web.  He outlined a three pronged approach to fixing it: 
    - Safe JavaScript subsets, such as ADsafe, Caja, and Cajita
    - Small browser improvements, like JSONRequest, and vats (where only part of the dom is accessible)
    - Massive browser improvements, like replacing JavaScript altogether with a new secure language, and creating a common text protocol to replace HTTP
    I agree that the Internet and it's components were not designed for many of the things we are doing with it today, but I don't believe that we need to start over.  The new and exciting applications that come out every day will push the technology toward where it needs to be.
  • An Introduction to the YUI Library by Eric Miraglia (Yahoo!): This session was a great look into YUI.  I've been using YUI in several of my demos, but I've only scratched the surface of what it's capable of.  Some good take-aways were the YUI compressor, css base and reset, YUI CSS Grid Builder, and the YUI profiler.
  • Enterprise Comet: Real-Time, Real-Time, or Real-Time Web 2.0? by Jonas Jacobi (Kaazing):  This was pretty much a condensed version of the previous presentation.  We found out that they aren't showing any examples or demos because they are in private beta right now.
  • jMaki as an AJAX Mashup Framework by Arun Gupta (Sun): This was an interesting concept.  jMaki is a level of abstraction that allows objects from different JavaScript libraries to interact with each other.  I'm not sure if we could use this in ASP.NET, but it's still a very cool concept.
  • Understanding the Top Web 2.0 Attack Vectors by Danny Allan (Watchfire): This was another session where there wasn't enough room for everyone.  We didn't bother waiting in the hallway, as we couldn't even hear the speaker.  I look forward to seeing this one on the DVD.
  • Writing Large Web Applications Using the YUI by Christian Heilmann (Yahoo!):  I had to stand outside in the hallway just to listen to the speaker on this one.  It sounded like a great session, but I couldn't take notes or see what he was going over.  Hopefully I'll be able to see it when the DVD comes out.
  • OpenAjax Gadgets & Widgets by Stewart Nickolas (IBM): This was my introduction to the OpenAjax Alliance.  They appear to have the same goal as jMaki, only this is an effort to get everyone to collaborate and contribute.  Stewart showed us a really slick mashup and gadget editor that was oriented toward developers.  I'm not sure where to find this editor, although I did find this.

Day 3:

  • Now Playing: Desktop Apps in the Browser! by Coach Wei & Bob Buffone (Nexaweb): This was another mildly entertaining ping-pong keynote from Nexaweb.
  • DreamFace: The Ultimate Framework for Creating Personalized Web 2.0 Mashups by Olivier Poupeney (DreamFace): I think this was the first session where the speaker actually put together a demo in front of us.  Not only was this refreshing, it was a very informative.  The framework is looks impressive.  I might try making a DreamFace gadget using some Atalasoft controls in the future.
  • The Digital Black Belt’s Guide to Building Secure ASP.NET AJAX Applications by Joe Stagner (Microsoft): This was the most informative and beneficial session that I went to these three days.  Some great take-aways were WebScarab, Internet Explorer 8's DOM explorer, ViewState Decoders, and ConTEXT.  His slides can be found here.

 

Posted by David Cilley | 3 Comments
Filed under: , , ,

Passing DOM elements from one frame to another

I came across an interesting problem with Internet Explorer and Safari this week:

I wanted to take an arbitrary html element that had been created on a page, and send it to an iframe that was on the same page.

I created a simple test page with an iframe, a div tag, and a button.  After a few lines of JavaScript, I had it working in FireFox.  When I tested it in IE 7, it gave me an "Invalid argument" exception, and didn't specify what was invalid.  This doesn't work in Safari either.  Opera handles it exactly like FireFox.

After tinkering with it for a while, I thought it might have something to do with the ownerDocument property on the DOM element. I thought that if I could change that property, that I may be able to get it to work.  You can't change this property directly... so I thought maybe by removing the object from the parent object, it would allow me to append it to another document.  This doesn't work either.

Everything that I tried ended up not working, so as a last resort, I decided to create a copy of the element on the child frame.  This works for me, but could be missing some attributes that may be important to someone else.

I made a test page that allows you to test both the direct appendChild method, and the copy code below, while logging all errors that occur.  You can see that this test page works in both IE 7 and Safari when the checkbox is checked.

Here is the code that I used to copy the element on the child frame's document:

// o : DOM object to copy
// childDoc : document object from the child frame
function CopyToChildObject(o, childDoc){
   // create a new object on the other document
   var n = childDoc.createElement(o.tagName);
  
   // copy everything inside this tag
   n.innerHTML = o.innerHTML;
  
   // copy attributes
   for (var i in o){
      try{
         n.setAttribute(i, o.getAttribute(i));
      }
      catch (e){
         // do nothing with errors
      }
   }

   // copy style  
   for (var i in o.style){
      try{
         n.style[i] = o.style[i];
      }
      catch (e){
          // do nothing with errors
      }
   }
  
   return n;
}
Posted by David Cilley | 1 Comments
Filed under: ,

Using YUI Animation to slide AJAX thumbnails in and out

One of the easiest usability improvements that can be made to an imaging application, is the ability to hide the thumbnails from view when they are no longer needed.  Most users would prefer to see as much of the image, and as little of the application on the screen as possible.

Using YUI's Animation Utility, you can accomplish this task, while adding a little more feedback that something is happening.

The thumbnails and the main viewer are placed in separate table cells, so that the whole table can be fit to the width of the browser (a common requirement for imaging apps).  The object that will be used for the animation, is a div tag (slideAnim in this case) that holds the thumbnails, and clips the content if it's too large. Keep in mind that height and width must be defined on all objects up the tree, if percentage based values need to be used.

Here's how I did it:

<!-- YUI Dependencies -->
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.0/build/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.5.0/build/animation/animation-min.js"></script>

<table style="width:100%;height:630px;">
<tr>
   <td valign="top">
      <div id="slideAnim" style="overflow:hidden; width:202px; height:630px;">
         <cc1:webthumbnailviewer id="WebThumbnailViewer1" runat="server" width="200px" height="600px" viewerid="WebImageViewer1" thumbsize="160, 120"></cc1:webthumbnailviewer>
      </div>
   </td>
   <td style="width:100%;" valign="top">
      <cc1:webimageviewer id="WebImageViewer1" runat="server" height="600px" width="100%" />
   </td>
</tr>
</table>       

<button id="demo-run" onclick="return false;">SlideIn/Out</button>

<script type="text/javascript">
   var _vis = true;
   var _outAttributes = { 
       width: { to: 0 } 
   };

   var _inAttributes = { 
       width: { to: 202 } 
   };
    
   // create animation objects
   var _animOut = new YAHOO.util.Anim('slideAnim', _outAttributes, 1, YAHOO.util.Easing.easeOut); 
   var _animIn = new YAHOO.util.Anim('slideAnim', _inAttributes, 1, YAHOO.util.Easing.easeIn); 
    
   // bind events
   YAHOO.util.Event.on('demo-run', 'click', slide);
   _animOut.onComplete.subscribe(onSlideOut);
   _animIn.onComplete.subscribe(onSlideIn);
    
   function onSlideOut(){
      _vis = false;
   }   

   function onSlideIn(){
      _vis = true;
   }    
    
   function slide(){
      if (_vis){
         slideOut();
      }
      else{
         slideIn();
      }
  
      return false;
   }
    
   function slideOut(){
      _animOut.animate();
   }

   function slideIn(){
      _animIn.animate();
   }
</script>

Slick Ajax Magnifier

magnifier screenshotRecently we had a customer ask if we had anything like our WinForms Magnifier MouseTool available on the WebForms side of things.  We get this question somewhat frequently, and it's always one of those features that seems to fall off the end of the development cycle to higher priority features.  The answer to this question has always been: "It's not inherantly supported, but you could probably make your own by floating another WebImageViewer over the mouse position."

I spent a little of my free time on it, and I've been able to confirm that this works.  Here are the steps that you need to take to get it to work:

  1. Create a WebForm with two WebImageViewer controls on it, one as the main viewer, and one as the magnifier.
  2. Wrap the magnifier WebImageViewer in a div tag, give the div tag an ID, and set the style to display:none.
  3. Attach a JavaScript MouseDownLeft event to the main viewer.
  4. In the MouseDownLeft event, attach JavaScript MouseMove and MouseUp events to both of the viewers, and set the style of the magnifier div to display:none.
  5. In the MouseMove events, set the position of the magnifier div to the position of the mouse - half the size of the magnifier.
  6. In the MouseUp event, detach the MouseMove and MouseUp events (by setting them to a blank function) and set the magnifier div style to display:none.
  7. The final step is to load the same image into both of the WebImageViewer controls, BestFit the main viewer, and keep the magnifier at zoom level 1.

I have attached a VS 2005 demo solution that shows how all of this is put together. This is a file system based web application with dll.refresh files pointing to the install location of DotImage.  If you do not have DotImage installed at that location, you will need to remove and re-add your references to the Atalasoft dll's.

Posted by David Cilley | 10 Comments
Attachment(s): MagnifierDemo.zip

Presentation: Introduction to AJAX in ASP.NET

I'd like to thank everyone that attended my presentation on AJAX for the Western Mass .NET Users Group last night.  I enjoyed presenting to the group and speaking with most of you.

Topics that were covered were:

  • What is AJAX?
  • AJAX in .NET with IFrames
  • ASP.NET AJAX Library
  • ASP.NET AJAX Extensions
  • ASP.NET AJAX Toolkit
  • AJAX Annotations Demo
  • Hero & Villain Card Builder

If you have any questions about the material covered, please don't hesitate to contact me. I'd be happy to answer any questions you might have.

For reference, I have attached my slides, enjoy! (Visual Studio 2005 and ASP.NET AJAX Extensions required)

Posted by David Cilley | 0 Comments
Filed under: , ,

Attachment(s): AJAXIntro.zip
More Posts Next page »