I Prefer Jim Developer James Schubert shares his code and his thoughts.

14Oct/12Off

Generating sprites with HTML5 canvas (node-canvas)

About a week ago, I posted about drawing simple shapes in HTML5. HTML5's Canvas isn't only useful on the client side.

About a year ago, I rewrote my Quaketracker mashup which pulls rss content from USGS and displays markers on a Google Map. For part of this rewrite, I wanted to use custom markers to indicate the magnitude of the earthquake, both numerically and colorfully. That task was more involved than I realized it would be. There doesn't seem to be any easy way to generate such a sprite in The Gimp or other graphics software, so I set out to generate a sprite of magnitudes 0.0 through 9.9 with a color range from yellow to orange using node-canvas. This actually made the task very simple.

base.png

marker

Before writing the script, I had to decide on an image. I created a pretty simple quote-bubble-like marker with a white background and a black outline. The image is 35x35 square with some transparency padding the actual marker image. This allows me to calculate the layout pretty easily. The white background on black outline gives me a very specific color to replace with the desired color of intensity.

node-canvas is a custom implementation of Canvas for node.js. When I originally wrote this script, I had a few issues compiling node-canvas. When I tweaked the script today, I had no issues. However, it seems like some things are not well-documented or incomplete (PixelArray).

Scripting it in node.js

A lot of times, if your script writes out to files, it's best to check for dependencies and cause your script to exit if they're not met. For example, I know this script is completely useless without node-canvas, so I can probe for the availability of that module and provide directions for installation. I'd do this before attempting to load any other functions from the canvas module. You could also check for versions at this point. If someone runs this script with node-canvas 0.6.0 it will still work, but this may not be the case with other modules. Here's how I start off the script.

var p = require('util').puts;

try {
    var probe = require('canvas');
} catch(e) {
    p("Cannot find node-canvas module.");
    p("Did you install dependencies:\n");
    p("\tnpm install -d");
    process.exit();
}

var fs = require('fs');
var Canvas = require('canvas');
var Image = Canvas.Image;

var canvas, ctx, baseImage, outImage, img;

I don't usually use 'global' variables like those in the last line of the above snippet for applications or client-side scripting. For a script like this, you could have everything as a global variable as long as the names don't clash with your imported modules.

To initialize shared variables, I like to define an init() function and call that function at the end of the script. This is pretty common in other languages like Ruby and Python, so why node server-side JavaScript?

var init = function() {
	// our working image is 35x35, we want 10x10 sheet of sprites
	canvas = new Canvas(35*10,35*10);
	ctx = canvas.getContext('2d');
	baseImage = __dirname + '/base.png';
	outImage = __dirname + '/markers.png';

	// pre-load the image
	img = new Image;
	img.onload = function() {
		processImage(fileProcessingComplete);
	};
	
	img.src = baseImage;
};

The cool thing about node-canvas is that the usage is very similar to the Canvas API implemented in browsers supporting the HTML5 Canvas. In fact, aside from the __dirname special property of node.js, you should be able to drop this init function into a browser with no problems. For this script, I want 100 different magnitudes to be generated from 0.0 through 9.9 (00-99 is 100) so the canvas is initialized to 10 rows and 10 columns of 35x35 squares. Just like a client-side Image object, we want to bind any post-processing of the image via the onload() function.

I'm going to Memento you a bit, and for that I'm sorry but the end is the easier concept. When all the processing is complete, we want to pull image data from the Canvas element and write it out to a file.

var fileProcessingComplete = function() {
	var out = fs.createWriteStream(outImage),
            stream = canvas.createPNGStream();

	stream.on('data', function(chunk){
	  out.write(chunk);
	});

	// when PNG stream is done, drain WriteStream
	stream.on('end', function(){
	  p('saved ' + outImage);
	  out.destroySoon();
	});
};

Accessing streams in node.js is usually done asynchronously. We can take chunks of data from the ReadStream of createPNGStream() and pump that to the WriteStream of the output file. When the data from the PNG is done streaming, we tell the output stream to finish writing its buffered data and destroy itself. It sounds intense, but it's pretty straightforward.

Processing the colors and marker images is the fun part. To match the function call in the init function, we just create a function with a callback in the standard way.

var processImage = function(cb) {
        // other code removed
	if(typeof cb === "function") {
		cb.call(this);
	}
}

If there's a callback function passed as a parameter, when processImage is finished, it will call that function passing the current scope as the execution scope and no parameters.

To draw out the custom marker, we'll want to define how we want to display the text and at what color we want to have the 0.0 marker start.

ctx.font = 'normal 12px Impact';
ctx.textAlign = 'center';
var color = [255, 255, 0, 235];

The color array represents red, green, blue, and alpha channel (opacity). This may look odd if you're used to specifying alpha channels in css as "rgba(255,255,0,0.92)", but that value of 235 will give roughly 92% opacity.

We can make the script run pretty quickly by using a second canvas 2d context for the recolor phases (which modifies every white pixel).

// use a temp Canvas and Context of 35x35 size.
var tempCanvas = new Canvas(35,35);
var tempCtx = tempCanvas.getContext('2d');

This is an optimization I made today when tweaking the original script. In the original version of this script, I would write the base image to the output canvas then iterate over 'NxN' pixels on the output canvas to change the color of one pixel. Using a temporary canvas context, we only have to iterate over 35 pixels wide by 35 pixels high for every new marker. This not only speeds up the process, but it could be a difference of having a script that won't run on slower machines.

For each of the desired 100 magnitudes, we'll want to find the x and y values (top-left pixel) to which we'll write the updated marker. For every 1/2 magnitude, the Green color value decrements by 13 points.

for(var magnitude = 0; magnitude <= 100; magnitude++) {
	var y = 35 * Math.floor(magnitude/10),
		x = ( 35*(magnitude % 10) );

	// This increments the color slightly
	if(magnitude % 5 == 0){
		color[1] = color[1] - 13;
	}

        // some code removed

	ctx.fillText("" + parseFloat(magnitude / 10, 1), x + (35/2), y + (35/2), 35);
}

The last line of the above snippet fills the magnitude text on the output canvas. This could be moved to the next part of the script for a further optimization: instead of just modifying each white pixel's color, we could recolor and apply the text.

The part of the script which changes the color according to the magnitude looks like this:

tempCtx.drawImage(img, 0, 0, 35,35);
var imgData = tempCtx.getImageData(0, 0, 35, 35);
var data = imgData && imgData.data;
if(data) {
	try {
		for(var pixel=0;pixel<data.length;pixel=pixel+4) {
			var red = data[pixel]
			var green = data[pixel+1];
			var blue = data[pixel+2];
			var alpha = data[pixel+3];

			if(red == 255 && green == 255 && blue == 255) {
				data[pixel] = color[0];
				data[pixel+1] = color[1];
				data[pixel+2] = color[2];
				data[pixel+3] = color[3];
			}
		}
	} catch (err) { console.error(err.message); }

	// Write our temp image data to the final canvas context
	/* imageData, dx, dy, sx, sy, sw, sh */
	ctx.putImageData(imgData,x,y,0,0, 35, 35);
}

Writing to the temporary canvas's 2d context gives us access to a data buffer which represents a 35x35 image where each pixel is represented by 4 bytes (rgba). Iterating this buffer by 4's, we can check that the given pixel (excluding alpha because that doesn't matter in this case) is white or rgba(255,255,255). If the pixel is white, we replace those 4 indexes with the values in our color array. When we're done iterating the buffer representing the 35x35 image, we can call putImageData on the output canvas. With that, the sprite is complete and our callback handles writing the file.

A note about putImageData

putImageData can be a strange beast. Notice the comment imageData, dx, dy, sx, sy, sw, sh... these are the parameters accepted by the function. Even the HTML5 Specs might make you go, "Uhm... what?" The idea is pretty simple once you understand it. You want to write a buffer (imgData) where the top-left pixel is at dx,dy. sx and sy (or dirtyX and dirtyY) represent a dirty rectangle of size sw x sh (or dirtyWidth by dirtyHeight).

If the imageData passed to the function is the same height and width as the target canvas context, you wouldn't want to overwrite every single pixel would you? You'd only want to overwrite pixels that have changed. Suppose in this script, I had an imageData object of 350x350 to complement the output canvas. When putting the modified imageData, I would have to calculate the current 35x35 box offset and write out the imageData to 0,0. As an example, if I wanted to overwrite the 35x35 box for the 9.9 magnitude marker with an imageData buffer of 350x350, I might write:

ctx.putImageData(imgData, 0, 0, 315, 315, 35, 35);

This would tell the context that although my buffer is 350x350, I've only changed a 35x35 rectangle starting at 315x15. Canvas is smart enough to only update those pixels rather than every pixel in the canvas.

On the other hand, in the script I've written, I only have an imageData object which represents a 35x35 buffer. So, I can tell putImageData to start at the point defined by (x,y) and write the buffer into a 35x35 rectangle. I like this method a little more, but it may not always be the case that you're writing a smaller buffer to the context.

The code

As always, the code for this blog post is available on github at jimschubert/blogs.

The sprite

Here's what the generated sprite looks like.

Markers generated for Quaketracker

flattr this!

13Oct/12Off

Getting started with jQuery plugins

jQuery plugins aren't that difficult to write, but some folks seem to have a hard time understanding how to get started, so I thought I would write a quick blog post about that. I'll show how to make a pretty simple keyword highlighter which also wraps your pre/code elements in a div with a header span element.

I've written a few jQuery plugins before. One of my favorites is jquery.empuzzle. This is a favorite not only because it's incredibly fun, but because it covers a few areas of jquery plugin development:

  1. default options
  2. merge options
  3. custom selectors
  4. plugin structure

It also covers a particular gotcha when your plugin interacts with images (i.e. images may load after document ready is fired). It's definitely worth checking it out, but I'd like to show a much simpler plugin here.

The structure

jQuery plugins are commonly written as an *immediate function*. This is creates a closure which executes immediate on the jQuery object itself:

;(function($) {
    // your plugin goes here.
})(jQuery);

I often wrap a jQuery plugin in semicolons as you see above to prevent any automatic semicolon insertion errors. This is something I would consider a 'best practice' and I suggest you do the same.

Within this structure, you'll want to extend jQuery to include your plugin's code.

$.fn is a pointer to the jQuery prototype object. When you execute a jQuery call like this:

var examples = $('.test-elements');

.. you will receive a jQuery object. This object represents an array of results matching the selector passed to the jQuery function. You can use console.log() from within your plugin function to see how you might interact with this object.

;(function($) {
    $.fn.example = function(arg) {
        console.log(this);
    };
})(jQuery);

You now have the start to a plugin called 'example'. You can execute this 'plugin' in the normal way.

var examples = $('.test-elements');
examples.example();

// same as: $('.test-elements').example();

Now, you'll want to iterate over each object found by the query selector and perform whatever functionality your plugin will provide. Let's define some functionality so we can have a (somewhat) useful plugin. We'll keep with the 'example' theme and write a plugin which stylizes a <code> tag and adds a small box in the top-left corner which says 'Example'. (I got this idea from Twitter's Bootstrap framework, see here). We'll also 'highlight' a few keywords within our example.

Here's what we want our final product to look like (after some hideous styling, of course):

For the goal screenshot, I've used Google's prettify plugin, which is licensed under Apache 2.0. I don't use this in the final product.

There will be a few things we'll need to do in this plugin:

  1. Merge settings specified by the user with our default settings
  2. Wrap the code block in a styled 'example' box
  3. "parse" the contents for a select set of keywords
  4. Wrap any found keywords with a style span

For the parsing bit, I'll just use a global RegEx replace on the contents of the code element.

Providing defaults

Providing defaults in a jQuery plugin and allowing a user to pass options to merge isn't difficult. Your plugin function will accept a parameter I like to call 'opts' and return a `this.each` function. Within the `.each` function, you'll extend the defaults object with those options passed into your plugin function and store the merged options into a new object called 'options'. A very simple example which writes out to console.log might look like this:

;(function($) {
    var defaults = {
        arr: [1,2,3,4,5,6],
        sample: "sample",
        style: "style"
    };

    var example = function(opts) {
        return this.each(function() {
            // merge opts with defaults into new object
            // so changes don't change defaults
            var options = $.extend({}, defaults, opts);
            console.log({ 
                elem: this.innerText,
                options: options
            });
        });
    };

    $.fn.example = example;
})(jQuery);

Here are some options we might want to allow for the example plugin:

  1. A class name for the box and a class name for the label
  2. An object mapping keywords to class names.
  3. A parse error callback
  4. A data attribute to specify a different 'Example' box text (so each element can define its own label)

Our defaults object will look like this (more or less):

    var defaults = {
        boxCss: "example-box",
	labelCss: "example-label",
        keywords: { "function":"blue", "this":"blue", "jQuery":"red" },
        onError: function() { },
        exampleAttr: ""
    };

Wrapping our targets

To make our lives simple, we'll first wrap the target element with a div for our example box. Then, we'll add a span just before the code block. We can improve performance by creating the wrapper div's HTML outside of the '.each' function. Because we're allowing the option of pulling the labeling span's text from the code element itself, we'll have to build that HTML within the '.each' function.

Here's what we have so far.

;(function($) {
    var defaults = {
        boxCss: "example-box",
        labelCss: "example-label",
        keywords: { "function":"blue", "this":"blue", "jQuery":"red" },
        onError: function() { },
        exampleAttr: ""
    };

    var example = function(opts) {
        var options = $.extend({}, defaults, opts);
        
        var wrapperHtml = [
            '<div class="',
            options.boxCss,
            '"></div>'
        ].join('');

        var labelArr = [
            '<span class="',
            options.labelCss,
            '">',
            "Example",
            '</span>'
        ];

        return this.each(function() {
            var labelText = "Example";
            if(options.exampleAttr) {
                labelText = $(this).attr(options.exampleAttr);
            }

            var labelHtml = [
                '<span class="',
                options.labelCss,
                '">',
                labelText,
                '</span>'
            ].join('');

            $(this).wrap(wrapperHtml);
            $(this).before(labelHtml);
        });
    };

    $.fn.example = example;
})(jQuery);

onError function

The onError function we allow in the options object does nothing more than provide a message if we don't have a code element to update with keyword highlights. We will supply a message to the callback function and return from the current iteration of '.each'. This goes at the beginning of the 'this.each' function.

// Find node for replacing text.
var replacementNode = $(this).children('code').andSelf().filter('code');

if(replacementNode.length == 0) {
    if(typeof options.onError === "function"){
        options.onError("No code nodes found");
    }
    
    return;
}

With the combination of children/andSelf/filter in the above code, we allow the plugin to operate on both 'pre' and 'code' elements.

When allowing users to pass functions as callbacks or to provide added functionality to a plugin, *always* check that it is a function.

"Parsing" contents

In the interest of saving some time, I'm going to use a regular expression to replace text within the code block. This isn't necessarily the most efficient way to achieve a budget syntax highlighter, but it will work.

To do this, we'll need to grab the text from our code node in which we'll replace keywords. Then, for every keyword in the hash of keywords-to-classes, we'll create a span to represent the highlighted keyword. Then, we'll do a search and replace using a global regular expression object, substituting each found keyword with the keyword wrapped in a span element. This goes at the end of the 'this.each' function.

    var originalText = replacementNode.text();
            
    Object.keys(options.keywords).forEach(function(key,idx){
        var replacement = [
            '<span class="',
            options.keywords[key],
            '">',
            key,
            '</span>'
        ].join('');
        
        var re = new RegExp(key,"g");
        originalText = originalText.replace(re, replacement);
    });

The only thing left to do is to add styles to your document and you're all set with a customized syntax highlighter!

There are a few issues with this simple implementation. First, keywords don't get merged (I'll leave that as an exercise for you). Second, this only highlights keywords. It doesn't provide regex matching for full-text highlights. In other words, this won't highlight comments.

If you're unfamiliar with jQuery plugins, or you're planning to begin writing a keyword highlighter plugin, this should at least get you started!

Try it out!

Check out the jsfiddle.

Get the code

The code for this blog post is available on Github.

flattr this!

15Jul/12Off

ServiceStack’s Markdown Razor Engine. Wow.

ServiceStack is a pretty sweet-looking alternative to WCF. It provides strongly-typed, well-designed, REST/RCP+SOAP services for .NET and Mono. Check out the README in the repository to see how ridiculously easy it is to setup a service. Unfortunately, I haven't yet had a chance to use the stack. I was looking through the code today and saw a Markdown-Razor Engine.

I was like, "Wha....?

Yes, they offer an alternative view engine which blends Markdown and Razor. These are my two favorite syntaxes, so I had to see how well it compiles.

I compiled the ServiceStack.RazorEngine from source (in Ubuntu 12.04/Mono, no less!) to test a simple markdown file.

I created this markdown file, example.md, to throw at the framework.

# Example ServiceStack.Markdown

## Showing @examples.Count items

@foreach (var item in examples) {
  - @item.Name: @item.Number 
}


**Note: The template requires a space after the item.Number value**

For more information, check out 
[the docs](http://www.servicestack.net/docs/markdown/markdown-razor).

Also, *don't forget* to check out the code at 
[gh:ServiceStack/ServiceStack](https://github.com/ServiceStack/ServiceStack)

Here are some _other_ attempts to **break** 
the markdown **generation_of_html**. **escaped\_underscore\_in\_tags**

A space after double-asterisk (&lt;strong&gt; tags) will ** break **

Now, when I say simple, I mean it has most elements of markdown that I use regularly. It also uses the markdown-razor foreach syntax which iterates over a collection of Example objects. This view is not strongly-typed, so this is equivalent to a dynamic Razor view.

The code to compile this view is really simple. You create the template engine, and provide a markdown page object which takes a full path (.e.g /fakepath/Debug) and the template contents (e.g. the above markdown snippet).

// Create the markdown-razor template compiler
MarkdownFormat format = new MarkdownFormat();
string contents = File.ReadAllText(template);
var page = new MarkdownPage(format, path, "example", contents );
format.AddPage(page);

Then, you create your view object (ViewBag?).

// Create our view container (ViewBag)
var view = new Dictionary<string, object>() 
{
	{ "examples", examples }
};

All that's left is to pass in the view object and compile it to html!

// Compile and output. 
// This can be redirected to html file 
// e.g. RazorExample.exe > output.html
var html = format.RenderDynamicPageHtml("example", view);
Console.WriteLine(html);

The Code

As always, the code for this post is available on github. More examples on the Markdown Razor syntax available in ServiceStack are available in the test project in ServiceStack's repository.

flattr this!

Tagged as: , No Comments
10Nov/10Off

System76 Ubuntu NetBook and Tethering on Verizon Droid (Android)

One of the first things I wanted to do when I got my netbook was to setup tethering.  However, I don't want to pay $30 more a month just to view the same pages I'd otherwise be viewing on my phone (i.e. Facebook, Twitter, Google News/Gmail).  The answer to this is an application called EasyTether.  It's currently only $10 on the Android Market.  Click here to download EasyTether.

The setup guide in EasyTether will walk you through getting the application configured on your phone and connecting that to your computer.  However, some people may have Linux-based NetBooks and not have the technical knowledge to write a simple script to startup 'easytether' in a console.  I'd like to provide those steps.  Please read on if you're interested!

Note: You *MUST* have your sudo password to proceed.

flattr this!

11Jun/10Off

Using DataAnnotation attributes to validate Membership password

As a follow-up to my post on DataAnnotations in ASP.NET Web Forms, I'd like to demonstrate yet another custom attribute. Although ASP.NET offers a CreateUserWizard, if your custom membership provider is way more complicated, you will probably be better off creating a control from scratch. If you go this route, you'll have to provide some of the functionality from the CreateUserWizard. Here is a simple attribute which checks *only* password complexity and builds an ErrorMessage without ever calling the CreateUser method.

This attribute can be added to a password property and validate against the *Default* Membership Provider.

Here is the code:


    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class MembershipPasswordRequiredAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if (value == null || !(value is string) || string.IsNullOrEmpty(value.ToString()))
            {
                return false;
            }

            MembershipSection membershipConfig = (MembershipSection)WebConfigurationManager
                                                                    .GetSection("system.web/membership");
            var providerSettings = membershipConfig.Providers[membershipConfig.DefaultProvider];
            string minLength = providerSettings.Parameters["minRequiredPasswordLength"];
            string minAlpha = providerSettings.Parameters["minRequiredNonalphanumericCharacters"];

            if (string.IsNullOrEmpty(this.ErrorMessage) && !string.IsNullOrEmpty(minLength))
            {
                string message = String.Empty;
                message = String.Format("Password must be at least {0} characters in length", minLength);
                if (!string.IsNullOrEmpty(minAlpha))
                {
                    message = String.Format("{0} and contain at least {1} special character", message,  minAlpha);
                }

                this.ErrorMessage = message;
            }
                /* Validate against your provider and return true or false */
        }
    }

Usage:

[MembershipPasswordRequired]
internal string Password {get;set;}

The cool thing about this attribute is that you can decorate a property without specifying the ErrorMessage and it will build one dynamically from your default membership. Of course, you can change this up if you're using multiple providers by getting the key of the current provider. But, the project I'm working on will always only have one provider, so this is how I'll leave it.

A caveat: You can only set the ErrorMessage property once. If you try to assign to it more than once, you will receive an exception telling you this.

I won't post the code for validating against the provider, because there are a number of ways to do this. Probably the safest way to do so is to use regex validation and pull that property from the provider's parameters and just return whether the regex matches the string or not.

Anyway, I thought this was a pretty cool usage of DataAnnotations, and hooking it up on a custom CreateUser control was trivial with the DataAnnotation validator (from a few posts ago).

flattr this!

17Jan/10Off

My Development Blog

When I began my internship with Agility Healthcare Solutions (now part of GE Healthcare), I thought it would be beneficial to keep a blog of my activities. Originally, I intended to keep a blog of problems I found or areas I found difficult, in case I ever encountered these problems again. Then, I realized my blog could be beneficial to others if I post random code. Earlier stages of my blog focus on difficulties I have had, while later stages are just examples of my code.

Please, feel free to follow my blog and make comments!

The Link: Blogger: James Schubert

flattr this!

Tagged as: No Comments
17Jan/10Off

Quake Tracker

The Project

I love Google products! Again, I am a huge fan of Google products, so I decided to explore the APIs and tools which Google offers.

Google Maps API is a framework which allows you to use Google's Maps in your own site, overlaying your own content onto the map. I wanted to create a map for a long time, but I didn't have any ideas for what I could overlay.

The Solution

When I was younger, I wanted to be a seismologist. I think this originated from the Pierce Brosnan movie, "Dante's Peak". Recently, my wife and I watched this film and it was not as factual as I once believed. Perhaps the best part of the movie, was when Brosnan, a volcanologist, is in the truck with the town's mayor. She is driving and approaches a river of lava. She asks, "Do you think it's safe to drive over it?" Brosnan says, "I don't know!" Wow!

With that background, I think it's understandable that I wanted to track earthquakes! Although this project isn't finished, I think it is fun to open it and see where the earth is quaking. This page uses jQuery and pulls an XML feed for earthquakes between M2 and M5 from usgs.gov, which is updated regularly. That means what you see is always up-to-date!

The Link: Quake Tracker

flattr this!

Tagged as: No Comments
17Jan/10Off

Calculator (Java/GWT)

The Project

My continuing goal in application development is to learn new technologies and explore my possibilities. I am a huge fan of Google products, so I decided to explore the APIs and tools which Google offers.

Google Web Toolkit is a framework which allows you to write web content in Java, and convert the code to JavaScript and HTML designed to work the same across browsers. I really enjoy using frameworks which make development faster and easier, such as Google Web Toolkit and jQuery.

This was my first attempt at creating anything with Google Web Toolkit. After following the example tutorials on Google's site, this was very easy.

The Link: Open in New Window/Tab or Open as pop-up

Here's a link to the code on github.

flattr this!

Tagged as: No Comments
17Jan/10Off

Meta Tag Generator

The Project

My wife and her friend are starting a copy writing business. I wrote this tool to help with SEO. Although simple, the application was written to make html code generation point-and-click.

This is a Web Start Application written in Java. It's fairly simple, with a Swing interface. It's not meant to include all meta tag possibilities. If you have any questions please feel free to contact me.

The Link: Meta Tag Generator

flattr this!

Tagged as: No Comments
17Jan/10Off

INFO 465 – Projects in IS

The class:

"The student's behavioral and technical skills developed in listed prerequisite courses are challenged by participating in a team systems development project. Appropriate computer-assisted software engineering (CASE) tools are used throughout the project, from requirement specification to implementation and testing." (VCU Course Description)

The assignment:

The semester project was to create a site for a two-man landscaping company. The assignment is designed to allow students to better understand EDI concepts, requirements gathering, inventory and customer management, double-entry accounting, and other real-world topics.

Many students decided to use WinForms on Access databases. To switch things up, I used ASP.NET with C# code on a SQL Server 2005 database. The site may look very simple, the reason for this is a requirement to have our applications be presentable on small screens such as netbooks or PDAs. While my site is not explicitly designed for mobile devices, it does look good on small screens such as iPod Touch.

The site which is currently hosted is not the complete site. In fact, it is the product about 2/3 the way through the semester. At this point in the semester, we were given another project. I developed both sites on my local machine, and never uploaded the final product. After graduation, I have been unable to upload the final project.

You may log into the system using the username "ralph" and the password "cheese".

The Link: INFO 465 Example (site currently down)

flattr this!

Tagged as: No Comments