I have spent much time on trying to find a solution for this, and the simplest I've found, using SignalR, is to use Hubs as a gateway to your Repository/API:
So, here's how the project would be set up:
ASP.NET MVC Controllers' Actions dish out entire pages.
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
The View should be wrapped in a Layout that loads a Knockout MVVM. The View then initializes the part of the MVVMs that need to be used (as in, lump all your MVVM script in one file, and the View initializes the SignalR connections, to avoid needless connections (the code below has the Script initializing itself)). The View also has KnockOut bindings attached to it.
MVVM:
function AddressViewModel(rid, nick, line1, line2, city, state, zip)
{
<!-- Modifiable Properties should be observable. This will allow Hub updates to flow to the View -->
var self = this;
self.rid = rid;
self.nick = ko.observable(nick);
self.line1 = ko.observable(line1);
self.line2 = ko.observable(line2);
self.city = ko.observable(city);
self.state = ko.observable(new StateViewModel(state.RID, state.Title, state.Abbreviation));
self.zip = ko.observable(zip);
}
function StateViewModel(rid, title, abbreviation)
{
<!-- States are a stagnant list. These will not be updated -->
var self = this;
self.rid = rid;
self.title = title;
self.abbreviation = abbreviation;
}
var Page = new function()
{
//Page holds all Page's View Models. The init function can be modified to start only certain hubs.
var page = this;
page.init = function()
{
page.Account.init();
}
page.Account = new function ()
{
//Account holds account-specific information. Should only be retrieved on an encrypted, secure, and authorized connection.
account.init = function()
{
account.Addresses.init();
}
//Addresses manages the calls to Hubs and their callbacks to modify local content.
account.Addresses = new function ()
{
//Connect to the hub, and create an observable list.
var addresses = this;
addresses.hub = $.connection.accountAddressHub;
addresses.list = ko.observableArray([]);
//Called on initial load. This calls the Index() function on the Hub.
addresses.init = function ()
{
addresses.hub.server.index();
}
//displayMode allows for dynamic changing of the template.
addresses.displayMode = ko.computed(function ()
{
return 'Address';
});
//Empty allows to prompt user instead of just showing a blank screen.
addresses.empty = ko.computed(function ()
{
if (addresses.list().length == 0)
{
return true;
}
else
{
return false;
}
});
//During initial load, unless if MVC provides the information with the View, the list will be empty until the first SignalR callback. This allows us to prompt the user we're still loading.
addresses.loading = ko.observable(true);
//The Hub's Index function ought to reach indexBack with a list of addresses. The addresses are then mapped to the list, using the local AddressViewModel. Sets initial load to false, as we now have addresses.
addresses.hub.client.indexBack = function (addressList)
{
$.map(addressList, function (address)
{
addresses.list.push(new AddressViewModel(address.RID, address.Nick, address.Line1, address.Line2, address.City, address.State, address.ZIP));
});
addresses.loading(false);
}
}
}
}
On Run Script (Place in Layout, Script File, or View, depending on needs or confirgurations per page)
$(function ()
{
//Configures what SignalR will do when starting, on receive, reconnected, reconnected, or disconnected.
$.connection.hub.starting(function ()
{
$('.updated').hide();
$('.updating').show();
});
$.connection.hub.received(function ()
{
$('.updating').hide();
$('.updated').show();
});
$.connection.hub.reconnecting(function ()
{
$('.updated').hide();
$('.updating').show();
});
$.connection.hub.reconnected(function ()
{
$('.updating').hide();
$('.updated').show();
});
//This will keep attempt to reconnect - the beauty of this, if the user unplugs the internet with page loaded, and then plugs in, the client reconnects automatically. However, the client would probably not receive any backlog - I haven't test that.
$.connection.hub.disconnected(function ()
{
setTimeout(function ()
{
$.connection.hub.start();
}, 5000); // Restart connection after 5 seconds.
});
//Apply knockout bindings, using the Page function from above.
ko.applyBindings(Page);
//Start the connection.
$.connection.hub.start(function ()
{
}).done(function ()
{
//If successfully connected, call the init functions, which propagate through the script to connect to all the necessary hubs.
console.log('Connected to Server!');
Page.init();
})
.fail(function ()
{
console.log('Could not Connect!');
});;
});
LayOut:
<!DOCTYPE html>
<html>
<head>
. . .
@Styles.Render( "~/Content/css" )
<!-- Load jQuery, KnockOut, and your MVVM scripts. -->
@Scripts.Render( "~/bundles/jquery" )
<script src="~/signalr/hubs"></script>
. . .
</head>
<body id="body" data-spy="scroll" data-target="#sidenav">
. . .
<div id="wrap">
<div class="container">
@RenderBody()
</div>
</div>
@{ Html.RenderPartial( "_Foot" ); }
</body>
</html>
View (Index):
@{
ViewBag.Title = "My Account";
}
<div>
@{
Html.RenderPartial( "_AddressesWrapper" );
}
</div>
_AddressesWrapper:
<div data-bind="with: Page.Account.Addresses">
@{
Html.RenderPartial( "_Address" );
}
<div id="Addresses" class="subcontainer">
<div class="subheader">
<div class="subtitle">
<h2>
<span class="glyphicon glyphicon-home">
</span>
Addresses
</h2>
</div>
</div>
<div id="AddressesContent" class="subcontent">
<div class="row panel panel-primary">
<!-- Check to see if content is empty. If empty, content may still be loading.-->
<div data-bind="if: Page.Account.Addresses.empty">
<!-- Content is empty. Check if content is still initially loading -->
<div data-bind="if:Page.Account.Addresses.loading">
<!-- Content is still in the initial load. Tell Client. -->
<div class="well well-lg">
<p class="text-center">
<img src="@Url.Content("~/Content/Images/ajax-loader.gif")" width="50px" height="50px" />
<strong>We are updating your Addresses.</strong> This should only take a moment.
</p>
</div>
</div>
<div data-bind="ifnot:Page.Account.Addresses.loading">
<!-- Else, if not loading, the Client has no addresses. Tell Client. -->
<div class="well well-lg">
<p class="text-center">
<strong>You have no Addresses.</strong> If you add an Addresses, you can view, edit, and delete it here.
</p>
</div>
</div>
</div>
<!-- Addresses is not empty -->
<div data-bind="ifnot: Page.Account.Addresses.empty">
<!-- We have content to display. Bind the list with a template in the Partial View we loaded earlier -->
<div data-bind="template: { name: Page.Account.Addresses.displayMode, foreach: Page.Account.Addresses.list }">
</div>
</div>
</div>
</div>
</div>
</div>
_Address:
<script type="text/html" id="Address">
<div class="col-lg-3 col-xs-6 col-sm-4 well well-sm">
<address>
<strong data-bind="text: nick"></strong><br>
<span data-bind="text: line1"></span><br>
<span data-bind="if: line2 == null">
<span data-bind="text: line2"></span><br>
</span>
<span data-bind="text: city"></span>, <span data-bind=" text: state().abbreviation"></span> <span data-bind="text: zip"></span>
</address>
</div>
</script>
The KnockOut script interchanges with a SignalR Hub. The Hub receives the call, checks the authorization, if necessary, and passes the call to the proper repository or straight to WebAPI 2 (this example). The SignalR Hub action then takes the results of the API exchange and determines which function to call, and what data to pass.
public class AccountAddressHub : AccountObjectHub
{
public override async Task Index()
{
//Connect to Internal API - Must be done within action.
using( AddressController api = new AddressController(await this.Account()) )
{
//Make Call to API to Get Addresses:
var addresses = api.Get();
//Return the list only to Connecting ID.
Clients.Client( Context.ConnectionId ).indexBack( addresses );
//Or, return to a list of specific Connection Ids - can also return to all Clients, instead of adding a parameter.
Clients.Clients( ( await this.ConnectionIds() ).ToList() ).postBack( Address );
}
}
}
The API Controller checks data integrity and sends a callback to the same SignalR Hub action.
public class AddressController
: AccountObjectController
{
...
// GET api/Address
public ICollection<Address> Get()
{
//This returns back the calling Hub action.
return Account.Addresses;
}
...
}
- Your .NET application will need to use the same functions as your javascript-ran site. This will allow a modification from any client to then propagate to however many clients are needed (that single client who just loaded, as in this example, or to broadcast to everyone, or anywhere in between)
The end result is that the Hub receives changes/calls, calls the API, the API verifies the data and returns it back to the Hub. The Hub can then update all clients. You then successfully have real-time database changes and real-time client changes. The only catch is any change outside of this system will require the clients to refresh, which means all client calls, especially changes, must go through Hubs.
If you need more examples, I would be happy to show some. Obviously, security measures should be taken, and the code here is obviously only a small example.