Contact Us
Free Trial
Secure Video Conferencing LiveSwitch Cloud
Secure Video Conferencing LiveSwitch Cloud

Build an Auction Website with WebSync

by Frozen Mountain Software, on March 01, 2013

Now that WebSync 4 is out of Beta, it’s time to put it to use!

This demo will be a simple auction website that demonstrates how WebSync enables you to keep your client’s browser up to date at all times even during intense back and forth bidding wars!

Before you start reading the code walk-through, you should download the WebSync packageand open the project found in the ‘\demos\WebSyncAuction\’ folder. Run the project and place some bids. Once you’ve got a functional overview of the demo the source code walk-through will be easier to follow.

The technology stack we’ve chosen for this demo is:

  • Visual Studio 2010 with .Net Framework 4.0
  • SqlServer Compact Edition 4.0 Database
  • EntityFramework for datalayer
  • WebSync for real time data updates
  • jQuery and jQuery UI for client side user interface

We’ll break down the demo into a few categories of development.

  1. Create a new Web Project
  2. Setup a basic auction data model and database with SqlCE 4 and EntityFramework
  3. Write some business logic to manage our auctions
  4. Write some WebSync events to handle subscriptions to our auction website
  5. Write a WebSync event to handle placing bids on auction items
  6. Setup our MasterPage and Default.aspx
  7. Write a JavaScript script client for the web page itself using jQuery and jQuery UI
  8. Testing the Auction app

Ok let’s get started!

Create a new Web Project in Visual Studio

To get started, let’s create a new “ASP.NET Web Application” in visual studio and name it “WebSyncAuction”.

  • Click File -> New -> Project…
  • Select “ASP.NET Web Application” from the list of available web project templates
  • Enter a project name like “WebSyncAuction”
  • Click Ok

To keep things simple for the purposes of this demo we are going to remove the Forms Authentication stuff from the project. They don’t hurt anything and you could certainly add authentication and login to this demo easily, but we want to start with a basic “pure” demo.

  • Delete the “Account” folder and all contents.
  • Delete the “About.aspx” page.
  • Delete Forms Authentication settings from the Web.Config file.
  • Delete login elements from the default MasterPage. (The login and logout navigation toolbar items.)
  • Delete “About” menu item from the default MasterPage.

Now we need to add some of the project references via Nuget.

  • Right click your project in the “Solution Explorer” within Visual Studio and select “Manage NuGet Packages…”
  • Search for ‘Microsoft.SqlServer.Compact’ and click Install. As of this tutorial, the current version is 4.0.8876.1
  • Search for ‘EntityFramework’ and click Install. As of this tutorial, the current version is 5.0.0.
  • Search for ‘jQuery’ and click Install. As of this tutorial, the current version is 1.9.0.
  • Search for ‘jQuery.UI.Combined’ and click Install. As of this tutorial, the current version is 1.10.0. (If you didn’t already add jQuery it will prompt you to do so)
  • Search for ‘jQuery.Templates’ and click Install. As of this tutorial, the current version is 0.1 Beta 1.

Now we need a few more references for this project which are not NuGet packages. The location of the WebSync assemblies will vary depending on your purchased license.

  • Right click your project in the “Solution Explorer” within Visual Studio and select “Add Reference”.
  • Click the “Browse” tab and locate your WebSync assemblies.
  • Add “FM.dll”, “FM.Server.dll”, “FM.WebSync.dll” and “FM.WebSync.Server.dll” to your project.

Setup a basic auction website data model and database with SqlCE 4 and EntityFramework

Before we can demo the WebSync part of an auction application, we need some basic foundational pieces in place. Let’s start by creating our database, building some data models and configuring EntityFramework.

  • Right click ‘App_Data’, select Add -> New Item. In the “Data” templates, select “SQL Server Compact 4.0 Local Database” and name your database “WebSyncAuctionDatabase.sdf”. Click “Add”.
  • Create two folders in your project, “Data” and “Migrations”
  • In your “Data” folder, add a class file named “Auction.cs”
  • In your “Data” folder, add a class file named “DemoDataContext.cs”
  • In your “Migrations” folder, add a class file named “Configuration.cs”

Now let’s create our Auction data models in our “Auction.cs” file. We’re going to create two data models: an “Auction” and an “AuctionBid”. Every “Auction” will maintain a historical list of all bids placed as a collection of “AuctionBid” records.


///
/// Each Auction is one item available for bidding on our website.
///
[DataContract]
public class Auction
{
[Key]
[DataMember(Name = "id")]
public int Id { get; set; }

[MaxLength(128)]
[DataMember(Name = "description")]
public string Description { get; set; }

[MaxLength(128)]
[DataMember(Name = "photoURL")]
public string PhotoURL { get; set; }

private DateTime _Expiration;
[DataMember(Name = "expiration")]
public DateTime Expiration
{
get
{
return new DateTime(_Expiration.Ticks, DateTimeKind.Utc);
}
set
{
if (value.Ticks == _Expiration.Ticks)
return;

_Expiration = new DateTime(value.Ticks, DateTimeKind.Utc);
}
}

[DataMember(Name = "minimumBidIncrement")]
public decimal MinimumBidIncrement { get; set; }

[MaxLength(32)]
[DataMember(Name = "currentBidUsername")]
public string CurrentBidUsername { get; set; }

[DataMember(Name = "currentBidPrice")]
public decimal CurrentBidPrice { get; set; }

///
/// Collection of bid history records.
///
public virtual ICollection BidHistory { get; set; }
}

///
/// Each AuctionBid represents one user's bid on a particular Auction.
///
[DataContract]
public class AuctionBid
{
[Key]
[DataMember(Name = "id")]
public int Id { get; set; }

[DataMember(Name = "auctionId")]
public int AuctionId { get; set; }
public virtual Auction Auction { get; set; }

[DataMember(Name = "timestamp")]
public DateTime Timestamp { get; set; }

[MaxLength(32)]
[DataMember(Name = "username")]
public string Username { get; set; }

[DataMember(Name = "bidPrice")]
public decimal BidPrice { get; set; }
}

Now we need to create out EntityFramework “DataContext” which will use our new connection string. We’ll add the DataContext to our “DemoDataContext.cs” file we created.


public class DemoDataContext : DbContext
{
///
/// Use the Database ConnectionString from our Web.Config called 'WebSyncAuctionDB'
///
public DemoDataContext()
: base("name=WebSyncAuctionDB") 
{ }
///
/// Our collection of contacts which represents one physical table in the database.
///
public DbSet Auctions { get; set; }
public DbSet AuctionBids { get; set; }
}

Now that we have some data models and a place to store them, we need to tell EntityFramework what behavior we want for database migrations. For the purposes of this demo, I want to EntityFramework to “seed” the database with some demo data anytime the application pool starts up. We’re going to build a custom “DbMigrationsConfiguration” in our “Configuration.cs” file we put in the Migrations project folder.


internal sealed class Configuration : DbMigrationsConfiguration
{
public Configuration()
{
// Do NOT set this to true
AutomaticMigrationsEnabled = false;
}

protected override void Seed(DemoDataContext context)
{
//  This method will be called after migrating to the latest version.

//  You can use the DbSet.AddOrUpdate() helper extension method
//  to avoid creating duplicate seed data. E.g.
//
context.Auctions.AddOrUpdate(
p => p.Description,
new Auction {
Description = "Mobile Phone",
PhotoURL = "/images/mobilephone.png",
Expiration = DateTime.UtcNow.AddMinutes(5).AddSeconds(53),
MinimumBidIncrement = 1.25M,
CurrentBidPrice = 1.00M,
CurrentBidUsername = "Ben"
},
new Auction {
Description = "Laptop",
PhotoURL = "/images/laptop.png",
Expiration = DateTime.UtcNow.AddMinutes(15).AddSeconds(2),
MinimumBidIncrement = 10.00M,
CurrentBidPrice = 100.00M
},
new Auction {
Description = "Printer",
PhotoURL = "/images/printer.png",
Expiration = DateTime.UtcNow.AddMinutes(10).AddSeconds(7),
MinimumBidIncrement = 2.5M,
CurrentBidPrice = 15.00M
}
);
}
}

Our data layer is almost complete; we just need to setup our configuration for EntityFramework to use our DemoDataContext and our custom DbMigrationsConfiguration class. These settings go in the web.config file.

First we need to declare the EntityFramework config section:


For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468

It’s now time to get EntityFramework to create the migration which will create our database tables in the local SqlCE database. My personal preference is to disable automatic migrations (you saw that in our custom migration configuration) and instead create my migrations explicitly when I’m ready for it. To create a migration manually you need to open the “Package Manager Console” from Visual Studio’s Tools Menu -> Library Package Manager -> Package Manager Console. The package manager console will already be configured by NuGet when you installed the EntityFramework NuGet package so that you can execute some EntityFramework commands.

  • Open the package manager console and type “Add-Migration CreateAuctionDataTables” and press enter. EntityFramework will automatically build your migration for you and add it to your project.
  • After you’ve reviewed the migration for accuracy, you can type “Update-Database” and press enter to have EntityFramework apply all pending migrations to your database.

Congratulations, you now have a working data layer for your auction application! In the next section of this tutorial we will put it to work and create a couple business methods to work with our auction data.

Write some business logic to manage our auctions

Before we start writing our business logic, let’s deal with how and when to create an EntityFramework DataContext and when to destroy it. A best practice for web projects is to use one DataContext throughout each HttpRequest. Each HttpRequest typically represents one request or operation by the end user and logically matches with one transaction against the database by default. There are lots of ways to approach this, however this has worked well for us in many EntityFramework web applications. We will create an instance of our DataContext in the Global.asax ‘Application_BeginRequest()’ method and then dispose of it in our ‘Application_EndRequest’ method.


protected virtual void Application_BeginRequest()
{
// We're going to create one EntityFramework data context to be used throughout
// this HttpRequest. We'll store it in the HttpContext so that it can be retrieved
// by our application API.
DemoDataContext context = new DemoDataContext();
context.Configuration.LazyLoadingEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
HttpContext.Current.Items["_EntityContext"] = context;
}

protected virtual void Application_EndRequest()
{
// Clean up our current context. EntityFramework data contexts grow in memory
// footprint as they are used and should not be kept around for longer than
// necessary.

var entityContext = HttpContext.Current.Items["_EntityContext"] as DemoDataContext;
if (entityContext != null)
entityContext.Dispose();
}

Now that we have a new DataContext stored in the HttpContext for every HttpRequest, we can easily access it via an internal property getter. This is really just convenience as it keeps our Linq statements nice and readable. Ok, start by adding an “AuctionManager” class to your project’s “Data” folder. We will write our business logic in this class to list open auctions and process incoming bids.


protected virtual void Application_BeginRequest()
{
// We're going to create one EntityFramework data context to be used throughout
// this HttpRequest. We'll store it in the HttpContext so that it can be retrieved
// by our application API.
DemoDataContext context = new DemoDataContext();
context.Configuration.LazyLoadingEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
HttpContext.Current.Items["_EntityContext"] = context;
}

protected virtual void Application_EndRequest()
{
// Clean up our current context. EntityFramework data contexts grow in memory
// footprint as they are used and should not be kept around for longer than
// necessary.

var entityContext = HttpContext.Current.Items["_EntityContext"] as DemoDataContext;
if (entityContext != null)
entityContext.Dispose();
}

Now that we have a new DataContext stored in the HttpContext for every HttpRequest, we can easily access it via an internal property getter. This is really just convenience as it keeps our Linq statements nice and readable. Ok, start by adding an “AuctionManager” class to your project’s “Data” folder. We will write our business logic in this class to list open auctions and process incoming bids.


public class AuctionsManager
{
///
/// A convenient way to grab our current entityframework data context
/// out of the HttpContext for use in the above CRUD methods for our
/// Auctions.
///
internal static DemoDataContext CurrentDataContext
{
get
{
DemoDataContext returnContext = null;

//
// This is created/disposed in Application_BeginRequest() and
// Application_EndRequest() when running in a web environment
//
// This is good practice with EntityFramework in a web
// environment as it ensures a healthy lifecycle for the data
// context.
//
if (HttpContext.Current != null)
{
returnContext = HttpContext.Current.Items["_EntityContext"] as DemoDataContext;
}

return returnContext;
}
}
}

Now that we have our “AuctionManager” class to hold our business logic, we’re going to create two methods for our auction application. First, let’s create a “ListActive” method to retrieve all active (non-expired) auctions. When we subscribe to the ‘/auctions’ channel we are really only interested in loading active (non-expired) auction items for display to the end user.


public static List ListActive()
{
var now = DateTime.UtcNow;

// Return only auctio Auctions with an expiration date in the future.
return CurrentDataContext.Auctions
.Where(auction => auction.Expiration > now)
.ToList();
}

Next, let’s create a “ProcessBid” method. We will use this method in a WebSync event for handling incoming auction bids. The business logic in this method will also do some basic validation of the request to prevent bidding on expired auctions or bidding below the current minimum bid.


public static Auction ProcessBid(int auctionId, AuctionBid auctionBid)
{
// Find the Auction we want to update in the database.
var dbAuction = CurrentDataContext.Auctions.Find(auctionId);

// Make sure Auction hasn't expired!
if (dbAuction.Expiration greater_than DateTime.UtcNow)
{
throw new Exception("Sorry, this auction has expired!");
}

// Make sure the new bid is greater than the current bid + minimum increment
if (auctionBid.BidPrice >= (dbAuction.CurrentBidPrice + dbAuction.MinimumBidIncrement))
{
dbAuction.CurrentBidPrice = auctionBid.BidPrice;
dbAuction.CurrentBidUsername = auctionBid.Username;
}
else
{
// If not, make sure its greater than the current bid (did they just fail to add the minimum increment or did another user outbid
them?)
throw new ArgumentException(string.Format("Your bid must exceed the previous bid of ${0} by this Auction's minimum bid increment of
${1}.", dbAuction.CurrentBidPrice, dbAuction.MinimumBidIncrement));
}

// Save a Bid History record for this bid.
var newAuctionBid = CurrentDataContext.AuctionBids.Create();
newAuctionBid.AuctionId = dbAuction.Id;
newAuctionBid.BidPrice = auctionBid.BidPrice;
newAuctionBid.Username = auctionBid.Username;
newAuctionBid.Timestamp = DateTime.UtcNow;
CurrentDataContext.AuctionBids.Add(newAuctionBid);

// Commit proposed DB changes.
CurrentDataContext.SaveChanges();

//
// This is the magic WebSync part. If we get to this line of code
// then none of the above database code threw an exception and our
// auction was in fact updated. Therefor we can let the world
// know it was modified by publishing to the channel we planned
// to use to notify clients about changes to this auction.
//
WebSyncServer.Publish(string.Format("/auctions/{0}", dbAuction.Id), Json.Serialize(dbAuction));

return dbAuction;
}

If the new bid was successfully saved, it will also be published via WebSync to immediately update all of our subscribed clients with the new current bid price and bidder name.

Write some WebSync events to handle subscriptions to our auction website

Now that we have all that basic stuff out of the way we can see how it comes together using WebSync events. The first set of WebSync events we are going to author will allow the WebSync client to subscribe to a list of open auctions and receive updates any time the bid price goes up.

This requires two WebSyncEvents, one BeforeSubscribe and one AfterSubscribe to customize the incoming subscriptions for the ‘/auctions’ channel and then return our initial set of data (currently active auctions). As the event names suggest a BeforeSubscribe event will fire before a client is subscribed to a channel and the AfterSubscribe event fires after the subscription is complete. During a BeforeSubscribe event you have the opportunity to modify the subscription request or cancel it completely. In our case we want to modify the subscription to include some extra channels for the client to receive updates about each auction we will return to it. In our AfterSubscribe event the client has already been subscribed to the set of channels we just constructed; now we just want to attach some extra data to the response that will be sent to the client.


public static List ListActive()
{
var now = DateTime.UtcNow;

// Return only auctio Auctions with an expiration date in the future.
return CurrentDataContext.Auctions
.Where(auction => auction.Expiration > now)
.ToList();
}

You might have noticed that we retrieve our data in the BeforeSubscribe event but don’t actually attach it to the response until AfterSubscribe. This is the best practice to maintain data integrity. If we set this initial data as part of the response too early there is still opportunity for the subscribe request to fail and our data would not be appropriate for that type of response, so you need to provide the data in the AfterSubscribe event. However you need to build the customized list of channel subscriptions in the BeforeSubscribe event where you still have the opportunity to modify the subscription request before it is processed, so you needed to load the data there. WebSync 4 provides some convenient features for passing data between WebSyncEvents using the SetDynamicValue and GetDynamicValue methods available on the WebSyncEventArgs class.

Setup our MasterPage and Default.aspx

Before we start coding the JavaScript we need to setup the page with the necessary JavaScript refences for WebSync, jQuery, jQueryUI and any other JavaScript we need. We’ll put all our JavaScript references in the MasterPage. Then we’ll clean up the Default.aspx to remove anything we don’t need for this demo.

First lets include jQuery and jQueryUI plus the necessary CSS stylesheet for jQueryUI components:


Include jQuery UI CSS Stylesheets
Include jQuery JavaScript References -->

Now let’s include some helpful JavaScript libraries:


Include jQuery extras
  • The jQuery.blockUI plugin is used to block DOM elements with a translucent light box effect and a loading indicator. Particularly when you first launch the application in Visual Studio, the WebSync subscribe may take a few seconds while EntityFramework runs the migration and seeds or re-seeds the demo data in the database. The blockUI plugin keeps the auction container dimmed with a nice little loading method.
  • jQuery.tmpl is a templating plugin for jQuery to generate DOM elements given a data object. While it is officially deprecated, for the purposes of a simple demo app like this it will save us some time and simplify the JavaScript code. In a real production system you might choose another approach for building DOM elements with jQuery.
  • jQuery.noticeWriter is a little utility for injecting notices with jQueryUI CSS decoration into the DOM and doing a little animation to grab the end user’s attention.
  • Moment.js helps us work with dates/times so we can have a cool countdown timer on our auction listings.

Now let’s include the WebSync JavaScript library and its only dependency fm.js:


Include the FM Core JavaScript Library (required for WebSync4)
Include the WebSync JavaScript Library
  • fm.js is where we keep functionality that is shared between multiple FrozenMountain libraries. This helps prevent duplicate code if you are using both WebSync and TheRest frameworks in your project.
  • Fm.websync.js is the WebSync client for JavaScript

While we are editing the MasterPage we also removed any extra menu items added automatically by Visual Studio. Now we can edit the Default.aspx page with a nice container div for us to put our auction items into:

Welcome to the WebSync Auction Demo!

This is a demo using WebSync and jQuery to build a simple auction website.

To read the full technical write-up explaining this demo visit Building an Auction Website with WebSync on our Blog.

Write a WebSync event to handle placing bids on auction items

So we’re now setup for client to subscribe to a list of auctions, but that’s not very interesting if we can’t also bid on those auctions and increase the current bid price. So let’s create a Server WebSyncEvent to handle those incoming bid requests.


[WebSync.WebSyncEvent(WebSync.EventType.BeforeService, "/auctions/{auctionId}", WebSync.FilterType.Template)]
public static void ServiceAuctionBid(object sender, WebSync.WebSyncEventArgs e)
{
var auctionId = e.Match.GetParameter("auctionId");

AuctionBid auctionPartial = Json.Deserialize(e.ServiceInfo.DataJson);

try
{
// Try to process this bid request. This bid request could
// fail if the user is not bidding high enough or if the
// auction has expired.
AuctionsManager.ProcessBid(auctionId, auctionPartial);
}
catch (Exception exc)
{
// Cancel this service request and send back an error msg.
e.Cancel(exc.Message);
}
}

Write a JavaScript script client for the web page itself using jQuery and jQuery UI

Add a new JavaScript file to your project’s “Scripts” folder and name it “AuctionListing.js”

Let’s start with a wireframe JavaScript component which will load when jQuery is ready.


(function () {
if (window.WebSyncAuctionDemo && window.WebSyncAuctionDemo.AuctionListing) {
return;
} else {
if (!window.WebSyncAuctionDemo) {
window.WebSyncAuctionDemo = {};
}
if (!window.WebSyncAuctionDemo.AuctionListing) {
window.WebSyncAuctionDemo.AuctionListing = {};
}
}
var controller = window.WebSyncAuctionDemo.AuctionListing;

controller.init = function (param) {
};

// Build any UI elements needed, dialogs, etc.
controller.buildUI = function () {
};

// Don't fire up this controller until the page is ready.
$(document).ready(function () {
controller.init();

controller.buildUI();
}); //END $(document).ready()
})();

Before we go any further, let’s have a moment of silence for our friend Internet Explorer. Internet Explorer does not define “console” and therefor “console.log” if you do not have your developer tools open in the browser. This frequently causes working JavaScript to fail in IE due to logging statements. To keep things simple, I’m just going to abstract out the logging to a separate method in my JavaScript controller. That way you can easily swap out your logging for your favorite implementation. I’ll create a simple implementation, you’re welcome to switch it out to some DOM logging or a fancier logging framework. Just in case we need it you’ll notice I added an ‘enableLogging’ property on the JavaScript component.


// Just a config switch to turn on/off logging within this
// custom WebSyncProxy implementation.
controller.enableLogging = true;

controller.log = function (logMessage) {
// Only log when safely possible. IE doesn't define console
// if the developer tools are closed which breaks your app.
if (controller.enableLogging && window.console && console.log) {
console.log(logMessage)}
}
};

Before we get into the implementation details of WebSync, let’s setup a few more foundational pieces. The first thing I’m going to add to our JavaScript component is a couple jQuery templates to facilitate construction of the user interface.


// Define some templates for use with the listing. These could
// also go in external files or your HTML markup, but we wanted
// a self contained JavaScript file.
controller.templates = {}
controller.templates['auctionBaseTemplate'] =
'
' + '' + '
 
' + '' + '' + '' + '' + '
'
;
controller.templates['auctionBidDialog'] =
'
' + '' + '
' + 'Bid Amount
' + '
' + 'Bidding Name
' + '
' + '
' + '
'
;

The first template represents the markup we will render for each active auction. The second template represents the markup we need for a bid dialog. Now we also want to hold references to some of things we are working with:

  • the auction container DOM element
  • a DOM element for displaying notices to the end user
  • a reference to our jQuery UI dialog after its created
  • a place to store a JavaScript timer to update our auction countdowns
  • a place to store our WebSync client instance.

Here’s the code I added to the JavaScript component to hold reference to all those things:


// Cache things we'll need to use more than once.
controller.auctionContainer = $('#auctionContainer');
controller.auctionNoticesContainer = null;
controller.auctionBidDialog = null;

controller.auctionEls = {};
controller.timer = null;

// A place to hold our WebSync client instance.
controller.websyncClient = null;

Now we are ready to setup WebSync and subscribe to our list of auctions! We’re going to do this in our JavaScript components init() method.


controller.init = function (param) {
controller.log('Begin AuctionListing.init()');

$("#auctionContainer").block();

// Initialize WebSync
controller.websyncClient = new fm.websync.client('websync.ashx');
controller.websyncClient.connect({
onSuccess: function (e) {
controller.log('Connected to WebSync successfully.');
},
onFailure: function (e) {
controller.log('Could not connect to WebSync (' + e.getException().message + ').');
},
onStreamFailure: function (e) {
controller.log('Lost connection to WebSync! Reconnecting...');
}
});

// Subscribe to new subscription set
var subscribeArgs = {
channel: '/auctions',
onSuccess: function (e) {
controller.log('Subscribed to master websync channel \'' + e.getChannel() + '\' successfully!');

var initialAuctionData = e.getMeta();
controller.log('Received ' + initialAuctionData.length + ' Active Auctions!');

// Add each auction in turn
for (auctionIndex in initialAuctionData) {
controller.addAuction(initialAuctionData[auctionIndex]);
}

// Make the 'Bid Now' button pretty with jQuery UI
$(".bidButton").button();

controller.timer = setInterval(controller.setExpirationTick, 1000);
},
onFailure: function (e) {
controller.log("Subscribe failure. " + e.getException().message);
},
onComplete: function (e) {
window.setInterval(function () {
$("#auctionContainer").unblock();
}, 500);
},
onReceive: controller.onReceive
};
controller.websyncClient.subscribe(subscribeArgs);
};

// Updates the expiration time for all of the auction items currently on
// the page.
controller.setExpirationTick = function () {
// Lets loop through each of our AuctionItems on our page and update
// the expiration countdown.
for (key in controller.auctionEls)
controller.auctionEls[key].trigger('tick');
};

controller.onReceive = function (e) {
};

If our WebSync subscribe was successful, we’re going to build our auction list and start a JavaScript timer to update our auctions’ countdowns in the user’s browser. The initial list of auctions was returned from our server side WebSyncEvent in the Meta property. This data can be retrieved within the onSuccess event handler with `e.getMeta()`. Let’s add a method to our JavaScript component that will take one Auction record and apply it to our jQuery template prepared earlier and then append the finished markup to our auction container element. We also need a method to wire up our auction events. Also note the timer above is simply firing a ‘tick’ event on each of our auctions. We’ll set up a handler for that tick event in our wireAuctionEvents() method so that the auction’s countdown timer is updated for every timer tick.


// Add a new auction to the auction container.
controller.addAuction = function (auctionRecord) {
// Try to get a reference to the auction item if it's already been added.
var auctionEl = controller.getCurrentAuctionItem(auctionRecord.id);

// If it hasn't been added, create it.
if (!auctionEl) {
// Generate a new auction element using jQuery templates.
auctionEl = $.tmpl(controller.templates['auctionBaseTemplate']);

// Add the element to the global cache. This allows us to refer to it easily
//     when performing an update later.
controller.auctionEls[auctionRecord.id] = auctionEl;

// Set the element's id attributes and attach the record to it using jQuery's
//     data handlign features.
auctionEl.attr('id', 'auctionItem-' + auctionRecord.id);
auctionEl.data('auctionRecord', auctionRecord);

// Add the new element to the auction container.
auctionEl.appendTo(controller.auctionContainer);

// Wire the events to the auction and then call an update.
controller.wireAuctionEvents(auctionEl);
controller.updateAuction(auctionRecord);
}
// If it's already added, just treat it like an update request.
else {
controller.updateAuction(auctionRecord);
}
};

// Update an auction already in the auction container.
controller.updateAuction = function (auctionRecord) {
// Try to get a reference to the auction item.
var auctionEl = controller.getCurrentAuctionItem(auctionRecord.id);

// If it exists, trigger its update method.
if (auctionEl) {
auctionEl.trigger('update', auctionRecord);
}
};

// Set up events for a single auction element.
controller.wireAuctionEvents = function (auctionEl) {
// Update event; causes the auction to refresh data.
auctionEl.on('update', function (e, auctionRecord) {
var el = $(this);

var currentBid = auctionEl.data('auctionRecord').currentBidPrice;
var newBid = auctionRecord.currentBidPrice;

if (newBid != currentBid) {
el.find('.currentBidPrice').stop(true, true).animate({
color: '#cc0',
fontSize: '24px'
}).delay(3000).animate({
color: '#161',
fontSize: '20px'
});
}

el.find('.description').text(auctionRecord.description);
el.find('.expiration').text(controller.formatExpiration(auctionRecord.expiration));
el.find('.currentBidPrice').text(controller.formatPrice(auctionRecord.currentBidPrice, 'USD'));
el.find('.currentBidUsername').text(!!auctionRecord.currentBidUsername ? auctionRecord.currentBidUsername : '- no bids -');
el.find('.photo').attr('src', auctionRecord.photoURL);
el.data('auctionRecord', auctionRecord);
});

// Update event for the auction's timer; only updates the timer.
auctionEl.on('tick', function (e) {
var el = $(this);

el.find('.expiration').text(controller.formatExpiration(el.data('auctionRecord').expiration));

// If the auction is over, trigger the delete event.
if (moment(el.data('auctionRecord').expiration) greater_than moment())
el.trigger('delete');
});

// Update event for the auction's timer; only updates the timer.
auctionEl.on('delete', function (e) {
var el = $(this);
el.off();

// Close the bid dialog if it's still up for the auction being deleted.
var bidDialogData = controller.auctionBidDialog.data('auctionRecord');
if (!!bidDialogData AND bidDialogData['id'] == el.data('auctionRecord')['id'])
controller.auctionBidDialog.dialog('close');

// Disable the bid button.
el.find('.bidButton').attr('disabled', 'disabled');

// Fade all the text/etc to grey. We don't use the callback because
//     it will run multiple times.
el.find('*').animate({
color: '#333'
}, 1000, 'swing');

// Nested animations.
el.animate({
backgroundColor: '#f55'
}, 1000,
function () {
el.animate({
borderWidth: 0,
padding: 0,
opacity: 0.25,
width: 10
}, 1000,
function () {
el.animate({
height: 0,
marginLeft: '-5px',
marginRight: '-5px',
marginTop: '150px',
paddingLeft: '5px',
width: 0
}, 1000, function () {
el.remove();
});
}
);
}
);
});

// Click event for the bidding button.
auctionEl.find('.bidButton').click(function (e) {
e.preventDefault();

// Update the auction bidding dialog with this element's record.
controller.auctionBidDialog.trigger('update', auctionEl.data('auctionRecord'));

// Open the dialog.
controller.auctionBidDialog.dialog('open');
});
};

Now that we have the ability to build our auction items list and update individual auction items, let’s handle incoming notifications from WebSync. For now we only really care about one kind of publication which are updates to any given auction items. These notifications were being published on channels that include the id for any given auction. So a channel would be like ‘/auctions/{auctionId}’. In the future we may add an administrative area to this demo and use other channels for new auctions (‘/auctions’) or to administratively deleted auctions (‘/auctions/ {auctionId}/delete’), but for now let’s just focus on auction updates.


controller.onReceive = function (e) {
// Got a message from WebSync!
var channel = e.getChannel(),
channelSegments = channel.split("/"),
auctionData = e.getData(),
recordId = auctionData.id;
// Figure out if this is a new, updated or deleted record.
if (channelSegments.length === 2) {
// For now, let's just log this as we haven't implemented
// an admin area or ability to add new auction yet.
controller.log('New auction with id ' + recordId);

// Maybe later we'll add a method to handle new auctions.
//me.onReceiveNew(store, record, metaData);
} else {
// Updated or Deleted Record
var deletedVerb = ((channelSegments.length > 3) AND (channelSegments[3] == 'delete'));
if (deletedVerb) {
// For now, let's just log this as we haven't implemented
// an admin area or ability to delete auctions yet.
controller.log('Delete auction id ' + recordId);

// Maybe later we'll add a method to delete auctions.
//me.onReceiveDeleted(store, record, metaData);
} else {
controller.log('Update auction id ' + recordId);
controller.updateAuction(auctionData);
}
}
};

The important things to notice is our use of e.getChannel() to see what channel our message was received on, and our use of e.getData() to retrieve the data delivered in this notification. If this is an update notification we pass the notification data to our updateAuction() method as we probably have a new bid price and new high bidder name.

Now let’s build a bid dialog with jQuery UI and use WebSync to ‘Service’ our request to the server. We are going to use another jQuery template to render our dialog markup and wire up the necessary events to handle any user interaction with the bid dialog.


// Build any UI elements needed, dialogs, etc.
controller.buildUI = function () {
// Create the inline messages container.
controller.auctionNoticesContainer = $('
', { id: 'auctionNoticesContainer' }).appendTo(controller.auctionContainer); // Create a bidding dialog. controller.auctionBidDialog = $.tmpl(controller.templates['auctionBidDialog']); // Handles 'update' events, which update the bid dialog before opening. // Could also update it if new data is received while a dialog is // open. controller.auctionBidDialog.on('update', function (e, auctionRecord) { // Alias var dialog = controller.auctionBidDialog; // Add to data so we can use later when submitting bids. dialog.data('auctionRecord', auctionRecord); // Convert the bid price/increments to floating point numbers at two decimals. var bidPrice = parseFloat(auctionRecord.currentBidPrice), bidIncrement = parseFloat(auctionRecord.minimumBidIncrement); // Set the starting default bid. dialog.find('.bidAmount').attr('value', bidPrice + bidIncrement); }); // Inititalize the actual auction bid dialog. controller.auctionBidDialog = controller.auctionBidDialog.dialog({ autoOpen: false, draggable: false, modal: true, resizable: false, stack: true, title: 'Enter Your Bid', open: function (event, ui) { var dialog = $(this), dlgMsgContainer = dialog.children(".dlgMessages"), btnPlaceBid = dialog.parent().find("button:eq(1)"); // Make sure we don't have any old validation messages lingering // in the DOM (if we display the bid dialog again faster than an // old error message animation has faded it away) dlgMsgContainer.empty(); // Make sure the dialog will resize vertically if we inject a // validation message into the dialog DOM. dialog.css("height", "auto"); // Let's allow a bid to be submitted with the enter key for easy testing. dialog.keypress(function (e) { if (e.keyCode === $.ui.keyCode.ENTER) { //dialog.parent().find("button:eq(1)").trigger("click"); btnPlaceBid.trigger("click"); } }); // Let put focus on the place bid button by default. btnPlaceBid.focus(); }, buttons: [{ // Cancel button; closes the dialog. text: 'Cancel', click: function (e) { controller.auctionBidDialog.dialog('close'); } }, { // Confirm bid button; processes the bid. text: 'Confirm', click: function (e) { // Get all the information needed. var dialog = controller.auctionBidDialog, dlgMsgContainer = dialog.children(".dlgMessages"); $(dialog).parent().block(); var auctionRecord = dialog.data('auctionRecord'), bidName = dialog.find('.bidName'), bidNameValue = bidName.val(), bidAmount = dialog.find('.bidAmount'), bidAmountValue = parseFloat(bidAmount.val()); // First validate out input values, make sure we don't have a blank bid or blank name. if (!bidNameValue || bidNameValue.length === 0) { $(dialog).parent().unblock(); dlgMsgContainer.writeError("Invalid bidding name."); bidName.focus(); return false; } if (!bidAmountValue || isNaN(bidAmountValue)) { $(dialog).parent().unblock(); dlgMsgContainer.writeError("Invalid bid amount."); bidAmount.focus(); return false; } // Submit the bid with websync. controller.submitBid(auctionRecord.id, bidAmountValue, bidNameValue); } } ] }); };

The `submitBid()` method is where we using our WebSync client to “service” a request to the server. This is really just a plain old ajax POST, except WebSync gives us a lot of extra capability by using “channels” to map our POST request to server side WebSyncEvents instead of using URLs like you would with REST. You could do this using Frozen Mountain’s TheRest framework if you preferred the URL style mapping instead of WebSync channels, but I wanted to keep this example focused on one framework for now. If we get a lot of feedback from the community, maybe we’ll expand this demo further over time.


// Publishes a bid for the specific auction id, with a given price and
// under a specified bidder name.
controller.submitBid = function (auctionId, bidPrice, bidName) {
// Send our new bid to the server!
var serviceArgs = {
channel: '/auctions/' + auctionId,
data: {
username: bidName,
bidPrice: bidPrice
},
onComplete: function (e) {
$(controller.auctionBidDialog).parent().unblock();
},
onSuccess: function (e) {
controller.auctionBidDialog.dialog('close');
controller.auctionNoticesContainer.writeAlert("Your bid was successful!");
},
onFailure: function (e) {
var dlgMsgContainer = controller.auctionBidDialog.children(".dlgMessages");
controller.auctionBidDialog.parent().unblock();
dlgMsgContainer.writeError(e.getErrorMessage());
}
};

controller.websyncClient.service(serviceArgs);
};

Testing the Auction App

Now that we have our auction demo complete, let’s do a little testing. In the real world, people will try everything on your auction website. So let’s try to simulate some of the ways people may interact with your website and make sure these conditions are handled appropriately.

  • Open multiple browsers (Chrome, Firefox, IE) and bid back and forth, watch them update. Each browser will cache the “Bid Name” you enter in the dialog in JavaScript. Place a bid from one browser with one name, then place a competing bid from another browser using a different name. Watch how both browsers update.
  • Try to perform overlapping bids, one will succeed and one will fail. Open the bid dialog in two browsers at the same time. Each browser should have the same bid price but from a different bid name. Now race! Try pressing the bid buttons as fast as you can. You’ll always have the first bid placed win and the second bidder will be told they bid too low.
  • Try to bid too low, it will fail. In our demo every auction has a current bid price, but also a minimum bid increment (take a look back at our demo data in the seed method). If you don’t bid high enough over the current bid price, your bid will fail.
  • Open the bid dialog, but wait for the auction to expire, then try to bid. For example open a the bid dialog, enter a valid bid, but then go for coffee break. The longest auction our demo has is just over 15 minutes. When you come back submit your bid. The bid will fail because the auction has expired while you were gone.

Hopefully this demo helps to illustrate the value of WebSync in real time applications. If you were to implement this auction demo with some kind of “frequent refresh” server polling mechanism you would be wasting bandwidth by returning the same data to the client often and wasting server resources processing those frequent incoming ajax requests on the server. WebSync reduces the wasted bandwidth and server load considerably so that you can handle a greater number of concurrent visitors with your server and saving you bandwidth costs with your host.

If you want to ask questions or discuss this demo, head on over to our google group!

 

Websync Free Trial

Topics:WebSync

Frozen Mountain Software

Started in 2008 and having grown to over 300 customers in 47 countries worldwide, Frozen Mountain provides cross-platform, licensed Real Time Communication (RTC) SDKs and RTC services that allow your organization to incorporate WebRTC audio/video streaming and mixing, selective forwarding, call signalling and much more into your applications.

Welcome! Grab a Coffee and Enjoy!

Thought provoking insights, ideas, and news tailored for WebRTC professionals,  live streaming junkies, and generally anyone considering adopting real-time video capabilities into their business.

Subscribe to Us